From 7aeb3bb475d0ce37cc39dce0ecd082e86cdd61c8 Mon Sep 17 00:00:00 2001 From: vasilito Date: Thu, 2 Jul 2026 13:41:03 +0300 Subject: [PATCH] build: capture build script auto-stash changes from 0.2.5 kernel/relibc/base build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build-redbear.sh script auto-stashes working tree changes in nested relibc and base source trees before running the build. These changes were captured by the failed kernel build attempt that hit the json-target-spec / kernel rust toolchain mismatch (fixed in 0.2.5 by creating the local 0.2.5 branch). Captured changes: - local/recipes/kde/* : KDE Frameworks 6 source CMakeLists whitespace changes from the autostash (preserved) - local/recipes/qt/qtbase/* : qtypes.h whitespace from the autostash (preserved) - local/sources/kernel/Cargo.lock : dependency lock from the kernel relibc rebuild attempt - local/sources/kernel/src/lib.rs : touched (mtime) by the build script's touch + make prefix command This is a bookkeeping commit — the actual code changes for the threading plan are on the 4 submodule branches (kernel, relibc, base, libredox) and will be pushed separately. 0.2.5 branch was created from 0.2.4 (HEAD cd3950072e) to continue Phase 0 of the multi-threading plan work in a clean branch. --- .../kde/kf6-kcmutils/source/CMakeLists.txt | 1 + .../kf6-kcolorscheme/source/CMakeLists.txt | 1 + .../kde/kf6-kcompletion/source/CMakeLists.txt | 1 + .../kf6-kconfigwidgets/source/CMakeLists.txt | 1 + .../kf6-kdeclarative/source/CMakeLists.txt | 2 +- .../kde/kf6-kiconthemes/source/CMakeLists.txt | 1 + .../kde/kf6-kjobwidgets/source/CMakeLists.txt | 1 + .../source/src/notifications_interface.cpp | 2 +- local/recipes/kde/kf6-ksvg/source.tar | Bin 86436 -> 85400 bytes .../kf6-ksvg/source/.git-blame-ignore-revs | 6 + local/recipes/kde/kf6-ksvg/source/.gitignore | 28 + .../kde/kf6-ksvg/source/.gitlab-ci.yml | 14 + local/recipes/kde/kf6-ksvg/source/.kde-ci.yml | 16 + .../kde/kf6-ksvg/source/CMakeLists.txt | 116 + .../kde/kf6-ksvg/source/KF6SvgConfig.cmake.in | 16 + .../kf6-ksvg/source/LICENSES/BSD-2-Clause.txt | 22 + .../kf6-ksvg/source/LICENSES/BSD-3-Clause.txt | 26 + .../kde/kf6-ksvg/source/LICENSES/CC0-1.0.txt | 121 + .../kf6-ksvg/source/LICENSES/GPL-2.0-only.txt | 319 ++ .../source/LICENSES/GPL-2.0-or-later.txt | 319 ++ .../kf6-ksvg/source/LICENSES/GPL-3.0-only.txt | 625 +++ .../source/LICENSES/LGPL-2.0-or-later.txt | 446 ++ .../source/LICENSES/LGPL-2.1-only.txt | 467 ++ .../source/LICENSES/LGPL-2.1-or-later.txt | 468 ++ .../source/LICENSES/LGPL-3.0-only.txt | 163 + .../LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + .../LICENSES/LicenseRef-KDE-Accepted-LGPL.txt | 12 + .../LICENSES/LicenseRef-Qt-Commercial.txt | 7 + .../source/LICENSES/Qt-LGPL-exception-1.1.txt | 21 + local/recipes/kde/kf6-ksvg/source/README.md | 43 + .../kf6-ksvg/source/autotests/CMakeLists.txt | 31 + .../source/autotests/data/background.svgz | Bin 0 -> 2879 bytes .../metadata.desktop | 19 + .../data/plasma/desktoptheme/testtheme/colors | 120 + .../plasma/desktoptheme/testtheme/element.svg | 1 + .../desktoptheme/testtheme/metadata.json | 18 + .../desktoptheme/testtheme/opaque/element.svg | 1 + .../plasma/desktoptheme/testtheme/plasmarc | 5 + .../source/autotests/framesvgtest.cpp | 131 + .../kf6-ksvg/source/autotests/framesvgtest.h | 35 + .../source/autotests/imagesettest.cpp | 114 + .../kde/kf6-ksvg/source/autotests/svgtest.cpp | 155 + .../recipes/kde/kf6-ksvg/source/metainfo.yaml | 19 + .../kde/kf6-ksvg/source/po/ar/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/ast/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/bg/libksvg6.po | 30 + .../kde/kf6-ksvg/source/po/ca/libksvg6.po | 30 + .../source/po/ca@valencia/libksvg6.po | 30 + .../kde/kf6-ksvg/source/po/cs/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/de/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/en_GB/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/eo/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/es/libksvg6.po | 30 + .../kde/kf6-ksvg/source/po/eu/libksvg6.po | 31 + .../kde/kf6-ksvg/source/po/fi/libksvg6.po | 27 + .../kde/kf6-ksvg/source/po/fr/libksvg6.po | 26 + .../kde/kf6-ksvg/source/po/ga/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/gl/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/he/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/hi/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/hu/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/ia/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/is/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/it/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/ja/libksvg6.po | 25 + .../kde/kf6-ksvg/source/po/ka/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/ko/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/lt/libksvg6.po | 30 + .../kde/kf6-ksvg/source/po/nl/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/nn/libksvg6.po | 30 + .../kde/kf6-ksvg/source/po/pl/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/pt_BR/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/ro/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/ru/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/sa/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/sk/libksvg6.po | 26 + .../kde/kf6-ksvg/source/po/sl/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/sv/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/tr/libksvg6.po | 28 + .../kde/kf6-ksvg/source/po/uk/libksvg6.po | 29 + .../kde/kf6-ksvg/source/po/zh_CN/libksvg6.po | 28 + .../kde/kf6-ksvg/source/src/CMakeLists.txt | 12 + .../kde/kf6-ksvg/source/src/Messages.sh | 11 + .../src/declarativeimports/CMakeLists.txt | 30 + .../src/declarativeimports/framesvgitem.cpp | 787 +++ .../src/declarativeimports/framesvgitem.h | 313 ++ .../declarativeimports/imagetexturescache.cpp | 58 + .../declarativeimports/imagetexturescache.h | 46 + .../src/declarativeimports/ksvgqml.qdoc | 6 + .../src/declarativeimports/ksvgqml.qdocconf | 38 + .../declarativeimports/managedtexturenode.cpp | 17 + .../declarativeimports/managedtexturenode.h | 27 + .../source/src/declarativeimports/svgitem.cpp | 297 ++ .../source/src/declarativeimports/svgitem.h | 136 + .../source/src/declarativeimports/types.h | 60 + .../kde/kf6-ksvg/source/src/ksvg/.krazy | 2 + .../kf6-ksvg/source/src/ksvg/CMakeLists.txt | 94 + .../kde/kf6-ksvg/source/src/ksvg/README | 22 + .../kde/kf6-ksvg/source/src/ksvg/framesvg.cpp | 1110 ++++ .../kde/kf6-ksvg/source/src/ksvg/framesvg.h | 512 ++ .../kde/kf6-ksvg/source/src/ksvg/imageset.cpp | 271 + .../kde/kf6-ksvg/source/src/ksvg/imageset.h | 245 + .../kf6-ksvg/source/src/ksvg/ksvg-index.qdoc | 21 + .../kde/kf6-ksvg/source/src/ksvg/ksvg.qdoc | 9 + .../kf6-ksvg/source/src/ksvg/ksvg.qdocconf | 35 + .../src/ksvg/private/framesvg_helpers.h | 83 + .../source/src/ksvg/private/framesvg_p.h | 204 + .../source/src/ksvg/private/imageset_p.cpp | 741 +++ .../source/src/ksvg/private/imageset_p.h | 174 + .../kf6-ksvg/source/src/ksvg/private/svg_p.h | 179 + .../kde/kf6-ksvg/source/src/ksvg/svg.cpp | 1209 +++++ .../kde/kf6-ksvg/source/src/ksvg/svg.h | 725 +++ .../kf6-ksvg/source/src/tools/CMakeLists.txt | 1 + .../source/src/tools/apply-stylesheet.sh | 287 + .../source/src/tools/currentColorFillFix.sh | 23 + .../inkscape extensions/plasmarename.inx | 15 + .../tools/inkscape extensions/plasmarename.py | 65 + .../tools/split-plasma-svgs/CMakeLists.txt | 16 + .../split-plasma-svgs/split-plasma-svgs.cpp | 321 ++ .../kde/kf6-ksvg/source/tests/frames.qml | 40 + .../kf6-ksvg/source/tests/selected_svg.qml | 35 + .../kde/kf6-ksvg/source/tests/shadows.qml | 44 + .../kde/kf6-ksvg/source/tests/testborders.qml | 93 + .../kf6-ktextwidgets/source/CMakeLists.txt | 1 + .../source/src/kswitchlanguagedialog_p.cpp | 6 +- .../kde/kf6-solid/source/CMakeLists.txt | 2 +- local/recipes/kde/kwin/source/.gitignore | 30 + local/recipes/kde/kwin/source/.gitlab-ci.yml | 19 + local/recipes/kde/kwin/source/.kde-ci.yml | 55 + local/recipes/kde/kwin/source/CMakeLists.txt | 538 ++ local/recipes/kde/kwin/source/CONTRIBUTING.md | 134 + .../source/KWinDBusInterfaceConfig.cmake.in | 10 + .../kde/kwin/source/LICENSES/BSD-3-Clause.txt | 26 + .../kde/kwin/source/LICENSES/CC0-1.0.txt | 121 + .../kde/kwin/source/LICENSES/GPL-2.0-only.txt | 319 ++ .../kwin/source/LICENSES/GPL-2.0-or-later.txt | 319 ++ .../kde/kwin/source/LICENSES/GPL-3.0-only.txt | 625 +++ .../kwin/source/LICENSES/GPL-3.0-or-later.txt | 625 +++ .../kwin/source/LICENSES/LGPL-2.0-only.txt | 446 ++ .../source/LICENSES/LGPL-2.0-or-later.txt | 446 ++ .../kwin/source/LICENSES/LGPL-2.1-only.txt | 467 ++ .../source/LICENSES/LGPL-2.1-or-later.txt | 175 + .../kwin/source/LICENSES/LGPL-3.0-only.txt | 163 + .../LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + .../LICENSES/LicenseRef-KDE-Accepted-LGPL.txt | 12 + .../recipes/kde/kwin/source/LICENSES/MIT.txt | 19 + local/recipes/kde/kwin/source/Mainpage.dox | 19 + local/recipes/kde/kwin/source/README.md | 50 + .../kde/kwin/source/autotests/CMakeLists.txt | 286 + .../source/autotests/data/Framework 13.icc | Bin 0 -> 583156 bytes .../data/Samsung CRG49 Shaper Matrix.icc | Bin 0 -> 6840 bytes .../kwin/source/autotests/drm/CMakeLists.txt | 57 + .../kwin/source/autotests/drm/mock_drm.cpp | 1290 +++++ .../kde/kwin/source/autotests/drm/mock_drm.h | 195 + .../source/autotests/effect/CMakeLists.txt | 20 + .../data/glplatform/V3D-V3D_4_2-desktop-2.1 | 17 + .../data/glplatform/VC4-V3D_2_1-desktop-2.1 | 17 + .../amd-catalyst-radeonhd-7700M-3.1.13399 | 18 + .../data/glplatform/amd-gallium-bonaire-3.0 | 21 + .../glplatform/amd-gallium-cayman-gles-3.0 | 22 + .../data/glplatform/amd-gallium-hawaii-3.0 | 21 + .../data/glplatform/amd-gallium-navi-4.5 | 21 + .../glplatform/amd-gallium-radeon-r9-290-4.5 | 21 + .../amd-gallium-radeon-rx-480-series-4.5 | 21 + .../amd-gallium-radeon-rx-550-series-3.1 | 21 + .../amd-gallium-radeon-rx-5700-xt-4.6 | 21 + .../amd-gallium-radeon-rx-580-series-4.5 | 21 + .../amd-gallium-radeon-rx-vega-56-4.5 | 21 + .../amd-gallium-radeon-rx-vega-64-4.5 | 21 + .../data/glplatform/amd-gallium-redwood-3.0 | 21 + .../data/glplatform/amd-gallium-tonga-4.1 | 21 + .../data/glplatform/intel-broadwell-gt2-3.3 | 19 + .../data/glplatform/intel-haswell-mobile-3.3 | 19 + .../glplatform/intel-ivybridge-desktop-3.0 | 20 + .../glplatform/intel-ivybridge-desktop-3.3 | 19 + .../glplatform/intel-ivybridge-mobile-3.3 | 19 + .../data/glplatform/intel-kabylake-gt2-4.6 | 19 + .../glplatform/intel-sandybridge-mobile-3.3 | 19 + .../data/glplatform/intel-skylake-gt2-3.0 | 19 + .../data/glplatform/lima-mali400-desktop-3.0 | 19 + .../effect/data/glplatform/llvmpipe-10.0 | 22 + .../effect/data/glplatform/llvmpipe-3.0 | 22 + .../effect/data/glplatform/llvmpipe-5.0 | 22 + .../glplatform/nvidia-geforce-gtx-560-4.5 | 19 + .../glplatform/nvidia-geforce-gtx-660-3.1 | 18 + .../glplatform/nvidia-geforce-gtx-950-4.5 | 18 + .../glplatform/nvidia-geforce-gtx-970-3.1 | 18 + .../glplatform/nvidia-geforce-gtx-970M-3.1 | 18 + .../glplatform/nvidia-geforce-gtx-980-3.1 | 18 + .../glplatform/panfrost-malit860-desktop-3.0 | 19 + .../qualcomm-adreno-330-libhybris-gles-3.0 | 16 + .../effect/data/glplatform/virgl-3.1 | 22 + .../autotests/effect/kwinglplatformtest.cpp | 214 + .../source/autotests/effect/timelinetest.cpp | 415 ++ .../autotests/effect/windowquadlisttest.cpp | 215 + .../autotests/integration/CMakeLists.txt | 216 + .../autotests/integration/activation_test.cpp | 1186 ++++ .../autotests/integration/activities_test.cpp | 134 + .../integration/bounce_keys_test.cpp | 185 + .../integration/buttonrebind_test.cpp | 490 ++ .../data/anim-data-delete-effect/effect.js | 14 + .../integration/data/example.desktop | 3 + .../integration/data/rules/force-maximize | 14 + .../data/rules/maximize-vert-apply-initial | 18 + .../integration/dbus_interface_test.cpp | 371 ++ .../integration/debug_console_test.cpp | 502 ++ .../integration/decoration_input_test.cpp | 774 +++ .../dont_crash_aurorae_destroy_deco.cpp | 136 + .../dont_crash_cancel_animation.cpp | 111 + .../integration/dont_crash_empty_deco.cpp | 106 + .../integration/dont_crash_glxgears.cpp | 93 + .../dont_crash_reinitialize_compositor.cpp | 139 + .../dont_crash_useractions_menu.cpp | 100 + .../integration/effects/CMakeLists.txt | 11 + .../desktop_switching_animation_test.cpp | 139 + .../effects/maximize_animation_test.cpp | 182 + .../effects/minimize_animation_test.cpp | 165 + .../popup_open_close_animation_test.cpp | 247 + .../effects/scripted_effects_test.cpp | 748 +++ .../effects/scripts/animationTest.js | 12 + .../effects/scripts/animationTestMulti.js | 24 + .../effects/scripts/completeTest.js | 22 + .../effects/scripts/effectContext.js | 6 + .../effects/scripts/effectsHandler.js | 18 + .../effects/scripts/fullScreenEffectTest.js | 8 + .../scripts/fullScreenEffectTestGlobal.js | 21 + .../scripts/fullScreenEffectTestMulti.js | 22 + ...rabAlreadyGrabbedWindowForcedTest_owner.js | 7 + ...rabAlreadyGrabbedWindowForcedTest_thief.js | 7 + .../grabAlreadyGrabbedWindowTest_grabber.js | 7 + .../grabAlreadyGrabbedWindowTest_owner.js | 7 + .../integration/effects/scripts/grabTest.js | 7 + .../effects/scripts/keepAliveTest.js | 13 + .../effects/scripts/keepAliveTestDontKeep.js | 13 + .../redirectAnimateDontTerminateTest.js | 21 + .../scripts/redirectAnimateTerminateTest.js | 21 + .../scripts/redirectSetDontTerminateTest.js | 22 + .../scripts/redirectSetTerminateTest.js | 22 + .../effects/scripts/screenEdgeTest.js | 3 + .../effects/scripts/screenEdgeTouchTest.js | 3 + .../effects/scripts/shortcutsTest.js | 3 + .../integration/effects/scripts/ungrabTest.js | 18 + .../effects/slidingpopups_test.cpp | 196 + .../toplevel_open_close_animation_test.cpp | 190 + .../integration/effects/translucency_test.cpp | 219 + .../autotests/integration/fakeinput_test.cpp | 347 ++ .../integration/fakes/CMakeLists.txt | 1 + .../fakes/org.kde.kdecoration3/CMakeLists.txt | 15 + .../fakedecoration_with_shadows.cpp | 64 + .../fakedecoration_with_shadows.json | 11 + .../integration/fractional_scaling_test.cpp | 158 + .../integration/generic_scene_opengl_test.cpp | 91 + .../integration/generic_scene_opengl_test.h | 29 + .../integration/globalshortcuts_test.cpp | 454 ++ .../integration/helper/CMakeLists.txt | 3 + .../autotests/integration/helper/kill.cpp | 35 + .../integration/idle_inhibition_test.cpp | 366 ++ .../integration/input_capture_test.cpp | 360 ++ .../integration/input_stacking_order.cpp | 157 + .../integration/inputmethod_test.cpp | 1017 ++++ .../autotests/integration/internal_window.cpp | 675 +++ .../integration/keyboard_input_test.cpp | 222 + .../integration/keyboard_layout_test.cpp | 531 ++ .../keymap_creation_failure_test.cpp | 88 + .../integration/kwin_wayland_test.cpp | 417 ++ .../autotests/integration/kwin_wayland_test.h | 1513 ++++++ .../integration/kwinbindings_test.cpp | 257 + .../integration/layershellv1window_test.cpp | 867 +++ .../autotests/integration/lockscreen.cpp | 818 +++ .../autotests/integration/maximize_test.cpp | 315 ++ .../integration/mouseactions_test.cpp | 268 + .../integration/move_resize_window_test.cpp | 1261 +++++ .../integration/no_global_shortcuts_test.cpp | 203 + .../integration/outputchanges_test.cpp | 2441 +++++++++ .../autotests/integration/placement_test.cpp | 569 ++ .../integration/plasma_surface_test.cpp | 299 ++ .../integration/plasmawindow_test.cpp | 285 + .../autotests/integration/platformcursor.cpp | 65 + .../integration/pointer_constraints_test.cpp | 364 ++ .../autotests/integration/pointer_input.cpp | 1944 +++++++ .../integration/quick_tiling_test.cpp | 2335 ++++++++ .../integration/scene_opengl_es_test.cpp | 22 + .../integration/scene_opengl_test.cpp | 22 + .../integration/screen_changes_test.cpp | 169 + .../integration/screencasting_test.cpp | 271 + .../integration/screenedges_test.cpp | 350 ++ .../autotests/integration/screens_test.cpp | 156 + .../integration/scripting/CMakeLists.txt | 2 + .../scripting/minimizeall_test.cpp | 155 + .../integration/scripting/screenedge_test.cpp | 278 + .../scripting/scripts/screenedge.js | 1 + .../scripting/scripts/screenedgetouch.qml | 10 + .../scripting/scripts/screenedgeunregister.js | 12 + .../scripting/scripts/touchScreenedge.js | 1 + .../integration/security_context_test.cpp | 205 + .../integration/showing_desktop_test.cpp | 236 + .../integration/stacking_order_test.cpp | 1138 ++++ .../integration/sticky_keys_test.cpp | 366 ++ .../autotests/integration/tabbox_test.cpp | 417 ++ .../integration/test_colormanagement.cpp | 571 ++ .../autotests/integration/test_helpers.cpp | 2608 +++++++++ .../integration/test_virtualkeyboard_dbus.cpp | 124 + .../autotests/integration/tiles_test.cpp | 895 ++++ .../integration/touch_input_test.cpp | 446 ++ .../integration/transient_placement.cpp | 551 ++ .../integration/virtual_desktop_test.cpp | 266 + .../integration/window_rules_test.cpp | 204 + .../integration/window_selection_test.cpp | 517 ++ .../autotests/integration/workspace_test.cpp | 399 ++ .../autotests/integration/x11_window_test.cpp | 3842 +++++++++++++ .../autotests/integration/x11keyread.cpp | 377 ++ .../integration/xdgshellwindow_rules_test.cpp | 3085 +++++++++++ .../integration/xdgshellwindow_test.cpp | 2808 ++++++++++ .../autotests/integration/xinerama_test.cpp | 71 + .../integration/xwayland_input_test.cpp | 270 + .../integration/xwaylandserver_crash_test.cpp | 124 + .../xwaylandserver_restart_test.cpp | 113 + .../source/autotests/libinput/CMakeLists.txt | 46 + .../source/autotests/libinput/device_test.cpp | 2489 +++++++++ .../autotests/libinput/gesture_event_test.cpp | 205 + .../autotests/libinput/key_event_test.cpp | 106 + .../autotests/libinput/mock_libinput.cpp | 1154 ++++ .../source/autotests/libinput/mock_libinput.h | 175 + .../autotests/libinput/pointer_event_test.cpp | 292 + .../autotests/libinput/touch_event_test.cpp | 121 + .../autotests/onscreennotificationtest.cpp | 124 + .../autotests/onscreennotificationtest.h | 24 + .../opengl_context_attribute_builder_test.cpp | 379 ++ .../autotests/output_transform_test.cpp | 477 ++ .../source/autotests/test_client_machine.cpp | 147 + .../source/autotests/test_colorspaces.cpp | 694 +++ .../kde/kwin/source/autotests/test_ftrace.cpp | 72 + .../kwin/source/autotests/test_gestures.cpp | 309 ++ .../kde/kwin/source/autotests/test_utils.cpp | 71 + .../autotests/test_virtual_desktops.cpp | 624 +++ .../autotests/test_window_paint_data.cpp | 179 + .../source/autotests/test_xcb_size_hints.cpp | 362 ++ .../kwin/source/autotests/test_xcb_window.cpp | 202 + .../source/autotests/test_xcb_wrapper.cpp | 480 ++ .../kde/kwin/source/autotests/test_xkb.cpp | 529 ++ .../kde/kwin/source/autotests/testutils.h | 54 + .../source/autotests/wayland/CMakeLists.txt | 2 + .../autotests/wayland/client/CMakeLists.txt | 292 + .../wayland/client/test_datasource.cpp | 259 + .../autotests/wayland/client/test_error.cpp | 138 + .../wayland/client/test_plasma_activities.cpp | 195 + .../client/test_plasma_virtual_desktop.cpp | 519 ++ .../wayland/client/test_plasmashell.cpp | 493 ++ .../client/test_pointer_constraints.cpp | 444 ++ .../client/test_server_side_decoration.cpp | 323 ++ .../test_server_side_decoration_palette.cpp | 187 + .../autotests/wayland/client/test_shadow.cpp | 266 + .../wayland/client/test_shm_pool.cpp | 210 + .../wayland/client/test_text_input_v2.cpp | 770 +++ .../wayland/client/test_wayland_appmenu.cpp | 171 + .../wayland/client/test_wayland_blur.cpp | 187 + .../wayland/client/test_wayland_contrast.cpp | 199 + .../wayland/client/test_wayland_filter.cpp | 149 + .../wayland/client/test_wayland_output.cpp | 335 ++ .../wayland/client/test_wayland_seat.cpp | 1957 +++++++ .../wayland/client/test_wayland_slide.cpp | 187 + .../client/test_wayland_subsurface.cpp | 1086 ++++ .../wayland/client/test_wayland_surface.cpp | 946 ++++ .../client/test_wayland_windowmanagement.cpp | 605 +++ .../wayland/client/test_xdg_decoration.cpp | 225 + .../wayland/client/test_xdg_foreign.cpp | 355 ++ .../wayland/client/test_xdg_output.cpp | 173 + .../wayland/client/test_xdg_shell.cpp | 597 +++ .../autotests/wayland/server/CMakeLists.txt | 162 + .../server/test_datacontrol_interface.cpp | 391 ++ .../autotests/wayland/server/test_display.cpp | 172 + .../server/test_inputmethod_interface.cpp | 640 +++ ...keyboard_shortcuts_inhibitor_interface.cpp | 212 + .../server/test_layershellv1_interface.cpp | 504 ++ .../server/test_no_xdg_runtime_dir.cpp | 42 + .../wayland/server/test_screencast.cpp | 184 + .../autotests/wayland/server/test_seat.cpp | 188 + .../wayland/server/test_tablet_interface.cpp | 596 +++ .../server/test_textinputv1_interface.cpp | 462 ++ .../server/test_textinputv3_interface.cpp | 796 +++ .../server/test_viewporter_interface.cpp | 192 + .../source/autotests/xcb_scaling_mock.cpp | 55 + .../source/cmake/modules/FindLibdrm.cmake | 105 + .../source/cmake/modules/FindLibeis-1.0.cmake | 37 + .../kwin/source/cmake/modules/FindXKB.cmake | 89 + .../source/cmake/modules/FindXwayland.cmake | 42 + .../kwin/source/cmake/modules/Findgbm.cmake | 104 + .../source/cmake/modules/Findhwdata.cmake | 25 + .../kwin/source/cmake/modules/Findlcms2.cmake | 75 + .../kde/kwin/source/data/CMakeLists.txt | 14 + .../kwin/source/data/icons/16-apps-kwin.png | Bin 0 -> 379 bytes .../kwin/source/data/icons/32-apps-kwin.png | Bin 0 -> 609 bytes .../kwin/source/data/icons/48-apps-kwin.png | Bin 0 -> 874 bytes .../kde/kwin/source/data/icons/CMakeLists.txt | 11 + .../kwin/source/data/icons/sc-apps-kwin.svgz | Bin 0 -> 3106 bytes .../kwin/source/data/org_kde_kwin.categories | 22 + .../kwin/source/data/update_default_rules.cpp | 61 + .../kde/kwin/source/doc/CMakeLists.txt | 9 + local/recipes/kde/kwin/source/doc/TESTING.md | 48 + .../kde/kwin/source/doc/coding-conventions.md | 86 + .../kwin/source/doc/desktop/CMakeLists.txt | 2 + .../kde/kwin/source/doc/desktop/index.docbook | 84 + .../source/doc/kwindecoration/CMakeLists.txt | 2 + .../kwin/source/doc/kwindecoration/button.png | Bin 0 -> 125222 bytes .../source/doc/kwindecoration/configure.png | Bin 0 -> 412 bytes .../source/doc/kwindecoration/decoration.png | Bin 0 -> 66698 bytes .../source/doc/kwindecoration/index.docbook | 151 + .../kwin/source/doc/kwindecoration/main.png | Bin 0 -> 155251 bytes .../source/doc/kwineffects/CMakeLists.txt | 2 + .../doc/kwineffects/configure-effects.png | Bin 0 -> 491 bytes .../doc/kwineffects/dialog-information.png | Bin 0 -> 745 bytes .../kwin/source/doc/kwineffects/index.docbook | 86 + .../kde/kwin/source/doc/kwineffects/video.png | Bin 0 -> 354 bytes .../source/doc/kwinscreenedges/CMakeLists.txt | 2 + .../source/doc/kwinscreenedges/index.docbook | 71 + .../kwin/source/doc/kwintabbox/CMakeLists.txt | 2 + .../kwin/source/doc/kwintabbox/index.docbook | 117 + .../source/doc/kwintabbox/taskswitcher.png | Bin 0 -> 137740 bytes .../source/doc/kwintouchscreen/CMakeLists.txt | 2 + .../source/doc/kwintouchscreen/index.docbook | 40 + .../doc/kwinvirtualkeyboard/CMakeLists.txt | 2 + .../doc/kwinvirtualkeyboard/index.docbook | 44 + .../moveresizerestriction.pdf | Bin 0 -> 100362 bytes .../moveresizerestriction.tex | 232 + .../source/doc/windowbehaviour/CMakeLists.txt | 2 + .../source/doc/windowbehaviour/index.docbook | 1214 +++++ .../source/doc/windowspecific/CMakeLists.txt | 2 + .../source/doc/windowspecific/Face-smile.png | Bin 0 -> 1225 bytes .../doc/windowspecific/akgregator-info.png | Bin 0 -> 29579 bytes .../windowspecific/akregator-attributes.png | Bin 0 -> 67397 bytes .../doc/windowspecific/akregator-fav.png | Bin 0 -> 125672 bytes .../windowspecific/config-win-behavior.png | Bin 0 -> 34486 bytes .../doc/windowspecific/emacs-attribute.png | Bin 0 -> 69493 bytes .../source/doc/windowspecific/emacs-info.png | Bin 0 -> 32770 bytes .../focus-stealing-pop2top-attribute.png | Bin 0 -> 56561 bytes .../source/doc/windowspecific/index.docbook | 1020 ++++ .../doc/windowspecific/knotes-attribute.png | Bin 0 -> 35453 bytes .../source/doc/windowspecific/knotes-info.png | Bin 0 -> 21435 bytes .../doc/windowspecific/kopete-attribute-2.png | Bin 0 -> 48108 bytes .../windowspecific/kopete-chat-attribute.png | Bin 0 -> 49175 bytes .../doc/windowspecific/kopete-chat-info.png | Bin 0 -> 30206 bytes .../source/doc/windowspecific/kopete-info.png | Bin 0 -> 29123 bytes .../doc/windowspecific/kwin-detect-window.png | Bin 0 -> 27744 bytes .../doc/windowspecific/kwin-kopete-rules.png | Bin 0 -> 46445 bytes .../doc/windowspecific/kwin-rule-editor.png | Bin 0 -> 40361 bytes .../kwin-rules-main-n-akregator.png | Bin 0 -> 47246 bytes .../doc/windowspecific/kwin-rules-main.png | Bin 0 -> 46711 bytes .../windowspecific/kwin-rules-ordering.png | Bin 0 -> 29609 bytes .../windowspecific/kwin-window-attributes.png | Bin 0 -> 61505 bytes .../windowspecific/kwin-window-matching.png | Bin 0 -> 52861 bytes .../doc/windowspecific/pager-4-desktops.png | Bin 0 -> 9655 bytes .../tbird-compose-attribute.png | Bin 0 -> 57895 bytes .../doc/windowspecific/tbird-compose-info.png | Bin 0 -> 34785 bytes .../windowspecific/tbird-main-attribute.png | Bin 0 -> 70988 bytes .../doc/windowspecific/tbird-main-info.png | Bin 0 -> 34040 bytes .../tbird-reminder-attribute-2.png | Bin 0 -> 50575 bytes .../windowspecific/tbird-reminder-info.png | Bin 0 -> 33493 bytes .../windowspecific/window-matching-emacs.png | Bin 0 -> 54411 bytes .../windowspecific/window-matching-init.png | Bin 0 -> 46204 bytes .../windowspecific/window-matching-knotes.png | Bin 0 -> 37378 bytes .../window-matching-kopete-chat.png | Bin 0 -> 50985 bytes .../windowspecific/window-matching-kopete.png | Bin 0 -> 49441 bytes .../window-matching-ready-akregator.png | Bin 0 -> 48545 bytes .../window-matching-tbird-compose.png | Bin 0 -> 49248 bytes .../window-matching-tbird-main.png | Bin 0 -> 53672 bytes .../window-matching-tbird-reminder.png | Bin 0 -> 54026 bytes .../kwin/source/examples/plugin/.gitignore | 1 + .../source/examples/plugin/CMakeLists.txt | 36 + .../source/examples/plugin/eventlistener.cpp | 34 + .../source/examples/plugin/eventlistener.h | 26 + .../kde/kwin/source/examples/plugin/main.cpp | 31 + .../kwin/source/examples/plugin/metadata.json | 5 + .../examples/plugin/metadata.json.license | 2 + .../source/examples/quick-effect/.gitignore | 1 + .../examples/quick-effect/CMakeLists.txt | 16 + .../package/contents/config/main.xml | 12 + .../package/contents/config/main.xml.license | 2 + .../package/contents/ui/config.ui | 39 + .../package/contents/ui/config.ui.license | 2 + .../quick-effect/package/contents/ui/main.qml | 45 + .../quick-effect/package/metadata.json | 20 + .../package/metadata.json.license | 2 + .../source/examples/quick-script/.gitignore | 1 + .../examples/quick-script/CMakeLists.txt | 16 + .../quick-script/package/contents/ui/main.qml | 94 + .../quick-script/package/metadata.json | 18 + .../package/metadata.json.license | 2 + .../kwin/source/kconf_update/CMakeLists.txt | 30 + ...6.0-delete-desktop-switching-shortcuts.cpp | 34 + .../kwin-6.0-remove-breeze-tabbox-default.cpp | 40 + .../kwin-6.0-reset-active-mouse-screen.cpp | 26 + ...n-6.1-remove-gridview-expose-shortcuts.cpp | 35 + .../kde/kwin/source/kconf_update/kwin.upd | 24 + local/recipes/kde/kwin/source/logo.png | Bin 0 -> 4008 bytes .../source/plasma-kwin_wayland.service.in | 8 + .../source/src/3rdparty/colortemperature.h | 289 + .../kde/kwin/source/src/3rdparty/xcursor.c | 519 ++ .../kde/kwin/source/src/3rdparty/xcursor.h | 81 + .../kde/kwin/source/src/CMakeLists.txt | 649 +++ local/recipes/kde/kwin/source/src/Messages.sh | 4 + .../kde/kwin/source/src/activation.cpp | 636 +++ .../kde/kwin/source/src/activities.cpp | 175 + .../recipes/kde/kwin/source/src/activities.h | 116 + local/recipes/kde/kwin/source/src/appmenu.cpp | 115 + local/recipes/kde/kwin/source/src/appmenu.h | 55 + local/recipes/kde/kwin/source/src/atoms.cpp | 90 + local/recipes/kde/kwin/source/src/atoms.h | 102 + .../kwin/source/src/backends/CMakeLists.txt | 8 + .../source/src/backends/drm/CMakeLists.txt | 29 + .../src/backends/drm/drm_abstract_output.cpp | 29 + .../src/backends/drm/drm_abstract_output.h | 34 + .../source/src/backends/drm/drm_backend.cpp | 476 ++ .../source/src/backends/drm/drm_backend.h | 97 + .../kwin/source/src/backends/drm/drm_blob.cpp | 42 + .../kwin/source/src/backends/drm/drm_blob.h | 33 + .../source/src/backends/drm/drm_buffer.cpp | 126 + .../kwin/source/src/backends/drm/drm_buffer.h | 61 + .../source/src/backends/drm/drm_colorop.cpp | 591 ++ .../source/src/backends/drm/drm_colorop.h | 215 + .../source/src/backends/drm/drm_commit.cpp | 322 ++ .../kwin/source/src/backends/drm/drm_commit.h | 125 + .../src/backends/drm/drm_commit_thread.cpp | 415 ++ .../src/backends/drm/drm_commit_thread.h | 74 + .../source/src/backends/drm/drm_connector.cpp | 519 ++ .../source/src/backends/drm/drm_connector.h | 162 + .../kwin/source/src/backends/drm/drm_crtc.cpp | 134 + .../kwin/source/src/backends/drm/drm_crtc.h | 66 + .../src/backends/drm/drm_egl_backend.cpp | 170 + .../source/src/backends/drm/drm_egl_backend.h | 71 + .../source/src/backends/drm/drm_egl_layer.cpp | 139 + .../source/src/backends/drm/drm_egl_layer.h | 44 + .../backends/drm/drm_egl_layer_surface.cpp | 685 +++ .../src/backends/drm/drm_egl_layer_surface.h | 125 + .../kwin/source/src/backends/drm/drm_gpu.cpp | 1116 ++++ .../kwin/source/src/backends/drm/drm_gpu.h | 200 + .../source/src/backends/drm/drm_layer.cpp | 159 + .../kwin/source/src/backends/drm/drm_layer.h | 56 + .../source/src/backends/drm/drm_logging.cpp | 10 + .../source/src/backends/drm/drm_logging.h | 13 + .../source/src/backends/drm/drm_object.cpp | 110 + .../kwin/source/src/backends/drm/drm_object.h | 75 + .../source/src/backends/drm/drm_output.cpp | 822 +++ .../kwin/source/src/backends/drm/drm_output.h | 105 + .../source/src/backends/drm/drm_pipeline.cpp | 731 +++ .../source/src/backends/drm/drm_pipeline.h | 177 + .../src/backends/drm/drm_pipeline_legacy.cpp | 250 + .../source/src/backends/drm/drm_plane.cpp | 309 ++ .../kwin/source/src/backends/drm/drm_plane.h | 127 + .../source/src/backends/drm/drm_pointer.h | 150 + .../source/src/backends/drm/drm_property.cpp | 168 + .../source/src/backends/drm/drm_property.h | 148 + .../src/backends/drm/drm_qpainter_backend.cpp | 66 + .../src/backends/drm/drm_qpainter_backend.h | 40 + .../src/backends/drm/drm_qpainter_layer.cpp | 141 + .../src/backends/drm/drm_qpainter_layer.h | 64 + .../src/backends/drm/drm_render_backend.h | 33 + .../backends/drm/drm_virtual_egl_layer.cpp | 161 + .../src/backends/drm/drm_virtual_egl_layer.h | 58 + .../src/backends/drm/drm_virtual_output.cpp | 147 + .../src/backends/drm/drm_virtual_output.h | 48 + .../kwin/source/src/backends/drm/overview.md | 51 + .../src/backends/fakeinput/CMakeLists.txt | 4 + .../backends/fakeinput/fakeinputbackend.cpp | 377 ++ .../src/backends/fakeinput/fakeinputbackend.h | 35 + .../backends/fakeinput/fakeinputdevice.cpp | 85 + .../src/backends/fakeinput/fakeinputdevice.h | 49 + .../src/backends/libinput/CMakeLists.txt | 25 + .../src/backends/libinput/connection.cpp | 772 +++ .../source/src/backends/libinput/connection.h | 98 + .../source/src/backends/libinput/context.cpp | 196 + .../source/src/backends/libinput/context.h | 79 + .../source/src/backends/libinput/device.cpp | 1207 +++++ .../source/src/backends/libinput/device.h | 931 ++++ .../source/src/backends/libinput/events.cpp | 382 ++ .../source/src/backends/libinput/events.h | 483 ++ .../backends/libinput/libinput_logging.cpp | 10 + .../src/backends/libinput/libinput_logging.h | 12 + .../src/backends/libinput/libinputbackend.cpp | 62 + .../src/backends/libinput/libinputbackend.h | 39 + .../src/backends/virtual/CMakeLists.txt | 7 + .../src/backends/virtual/virtual_backend.cpp | 190 + .../src/backends/virtual/virtual_backend.h | 74 + .../backends/virtual/virtual_egl_backend.cpp | 186 + .../backends/virtual/virtual_egl_backend.h | 79 + .../src/backends/virtual/virtual_logging.cpp | 9 + .../src/backends/virtual/virtual_logging.h | 11 + .../src/backends/virtual/virtual_output.cpp | 171 + .../src/backends/virtual/virtual_output.h | 56 + .../virtual/virtual_qpainter_backend.cpp | 116 + .../virtual/virtual_qpainter_backend.h | 68 + .../src/backends/wayland/CMakeLists.txt | 12 + .../src/backends/wayland/wayland_backend.cpp | 730 +++ .../src/backends/wayland/wayland_backend.h | 272 + .../src/backends/wayland/wayland_display.cpp | 446 ++ .../src/backends/wayland/wayland_display.h | 116 + .../backends/wayland/wayland_egl_backend.cpp | 342 ++ .../backends/wayland/wayland_egl_backend.h | 115 + .../src/backends/wayland/wayland_logging.cpp | 10 + .../src/backends/wayland/wayland_logging.h | 14 + .../src/backends/wayland/wayland_output.cpp | 566 ++ .../src/backends/wayland/wayland_output.h | 154 + .../wayland/wayland_qpainter_backend.cpp | 190 + .../wayland/wayland_qpainter_backend.h | 99 + .../source/src/backends/x11/CMakeLists.txt | 17 + .../kde/kwin/source/src/client_machine.cpp | 243 + .../kde/kwin/source/src/client_machine.h | 107 + .../kde/kwin/source/src/compositor.cpp | 1042 ++++ .../recipes/kde/kwin/source/src/compositor.h | 120 + .../kde/kwin/source/src/config-kwin.h.cmake | 33 + .../kwin/source/src/core/brightnessdevice.h | 33 + .../kde/kwin/source/src/core/colorlut3d.cpp | 50 + .../kde/kwin/source/src/core/colorlut3d.h | 42 + .../kwin/source/src/core/colorpipeline.cpp | 579 ++ .../kde/kwin/source/src/core/colorpipeline.h | 167 + .../source/src/core/colorpipelinestage.cpp | 48 + .../kwin/source/src/core/colorpipelinestage.h | 33 + .../kde/kwin/source/src/core/colorspace.cpp | 953 ++++ .../kde/kwin/source/src/core/colorspace.h | 310 ++ .../source/src/core/colortransformation.cpp | 112 + .../source/src/core/colortransformation.h | 47 + .../kde/kwin/source/src/core/drmdevice.cpp | 119 + .../kde/kwin/source/src/core/drmdevice.h | 52 + .../src/core/gbmgraphicsbufferallocator.cpp | 339 ++ .../src/core/gbmgraphicsbufferallocator.h | 28 + .../kwin/source/src/core/graphicsbuffer.cpp | 104 + .../kde/kwin/source/src/core/graphicsbuffer.h | 219 + .../src/core/graphicsbufferallocator.cpp | 20 + .../source/src/core/graphicsbufferallocator.h | 46 + .../source/src/core/graphicsbufferview.cpp | 107 + .../kwin/source/src/core/graphicsbufferview.h | 31 + .../kde/kwin/source/src/core/iccprofile.cpp | 581 ++ .../kde/kwin/source/src/core/iccprofile.h | 76 + .../kde/kwin/source/src/core/inputbackend.cpp | 29 + .../kde/kwin/source/src/core/inputbackend.h | 46 + .../kde/kwin/source/src/core/inputdevice.cpp | 97 + .../kde/kwin/source/src/core/inputdevice.h | 172 + .../kde/kwin/source/src/core/output.cpp | 500 ++ .../recipes/kde/kwin/source/src/core/output.h | 304 ++ .../kwin/source/src/core/outputbackend.cpp | 112 + .../kde/kwin/source/src/core/outputbackend.h | 106 + .../source/src/core/outputconfiguration.cpp | 27 + .../source/src/core/outputconfiguration.h | 99 + .../kde/kwin/source/src/core/outputlayer.cpp | 284 + .../kde/kwin/source/src/core/outputlayer.h | 183 + .../kde/kwin/source/src/core/pixelgrid.h | 61 + .../kwin/source/src/core/renderbackend.cpp | 193 + .../kde/kwin/source/src/core/renderbackend.h | 139 + .../kwin/source/src/core/renderjournal.cpp | 46 + .../kde/kwin/source/src/core/renderjournal.h | 35 + .../kde/kwin/source/src/core/renderloop.cpp | 322 ++ .../kde/kwin/source/src/core/renderloop.h | 132 + .../kde/kwin/source/src/core/renderloop_p.h | 62 + .../kde/kwin/source/src/core/rendertarget.cpp | 72 + .../kde/kwin/source/src/core/rendertarget.h | 44 + .../kwin/source/src/core/renderviewport.cpp | 211 + .../kde/kwin/source/src/core/renderviewport.h | 70 + .../kde/kwin/source/src/core/session.cpp | 57 + .../kde/kwin/source/src/core/session.h | 133 + .../source/src/core/session_consolekit.cpp | 363 ++ .../kwin/source/src/core/session_consolekit.h | 52 + .../kwin/source/src/core/session_logind.cpp | 361 ++ .../kde/kwin/source/src/core/session_logind.h | 52 + .../kde/kwin/source/src/core/session_noop.cpp | 74 + .../kde/kwin/source/src/core/session_noop.h | 35 + .../src/core/shmgraphicsbufferallocator.cpp | 148 + .../src/core/shmgraphicsbufferallocator.h | 20 + .../kwin/source/src/core/syncobjtimeline.cpp | 160 + .../kwin/source/src/core/syncobjtimeline.h | 65 + local/recipes/kde/kwin/source/src/cursor.cpp | 614 +++ local/recipes/kde/kwin/source/src/cursor.h | 198 + .../kde/kwin/source/src/cursorsource.cpp | 195 + .../kde/kwin/source/src/cursorsource.h | 101 + .../kde/kwin/source/src/dbusinterface.cpp | 469 ++ .../kde/kwin/source/src/dbusinterface.h | 270 + .../kde/kwin/source/src/debug_console.cpp | 1865 +++++++ .../kde/kwin/source/src/debug_console.h | 225 + .../kde/kwin/source/src/debug_console.ui | 497 ++ .../src/decorations/decoratedwindow.cpp | 358 ++ .../source/src/decorations/decoratedwindow.h | 108 + .../src/decorations/decorationbridge.cpp | 292 + .../source/src/decorations/decorationbridge.h | 82 + .../src/decorations/decorationpalette.cpp | 163 + .../src/decorations/decorationpalette.h | 74 + .../src/decorations/decorations_logging.cpp | 10 + .../src/decorations/decorations_logging.h | 12 + .../kwin/source/src/decorations/settings.cpp | 186 + .../kwin/source/src/decorations/settings.h | 63 + .../kwin/source/src/dpmsinputeventfilter.cpp | 202 + .../kwin/source/src/dpmsinputeventfilter.h | 60 + .../kde/kwin/source/src/effect/anidata.cpp | 110 + .../kde/kwin/source/src/effect/anidata_p.h | 73 + .../source/src/effect/animationeffect.cpp | 993 ++++ .../kwin/source/src/effect/animationeffect.h | 532 ++ .../kde/kwin/source/src/effect/effect.cpp | 548 ++ .../kde/kwin/source/src/effect/effect.h | 1009 ++++ .../kwin/source/src/effect/effectframe.cpp | 362 ++ .../kde/kwin/source/src/effect/effectframe.h | 203 + .../kwin/source/src/effect/effecthandler.cpp | 1703 ++++++ .../kwin/source/src/effect/effecthandler.h | 1130 ++++ .../kwin/source/src/effect/effectloader.cpp | 500 ++ .../kde/kwin/source/src/effect/effectloader.h | 355 ++ .../src/effect/effecttogglablestate.cpp | 263 + .../source/src/effect/effecttogglablestate.h | 140 + .../kwin/source/src/effect/effectwindow.cpp | 529 ++ .../kde/kwin/source/src/effect/effectwindow.h | 991 ++++ .../kde/kwin/source/src/effect/globals.h | 449 ++ .../kde/kwin/source/src/effect/logging.cpp | 10 + .../kde/kwin/source/src/effect/logging_p.h | 14 + .../source/src/effect/offscreeneffect.cpp | 438 ++ .../kwin/source/src/effect/offscreeneffect.h | 135 + .../source/src/effect/offscreenquickview.cpp | 673 +++ .../source/src/effect/offscreenquickview.h | 179 + .../kwin/source/src/effect/quickeffect.cpp | 643 +++ .../kde/kwin/source/src/effect/quickeffect.h | 200 + .../kde/kwin/source/src/effect/timeline.cpp | 209 + .../kde/kwin/source/src/effect/timeline.h | 270 + .../recipes/kde/kwin/source/src/effect/xcb.h | 55 + local/recipes/kde/kwin/source/src/events.cpp | 747 +++ .../kde/kwin/source/src/focuschain.cpp | 298 ++ .../recipes/kde/kwin/source/src/focuschain.h | 238 + local/recipes/kde/kwin/source/src/ftrace.cpp | 123 + local/recipes/kde/kwin/source/src/ftrace.h | 107 + .../recipes/kde/kwin/source/src/gestures.cpp | 373 ++ local/recipes/kde/kwin/source/src/gestures.h | 157 + .../kde/kwin/source/src/globalshortcuts.cpp | 345 ++ .../kde/kwin/source/src/globalshortcuts.h | 203 + local/recipes/kde/kwin/source/src/group.cpp | 147 + local/recipes/kde/kwin/source/src/group.h | 88 + .../kwin/source/src/helpers/CMakeLists.txt | 3 + .../source/src/helpers/killer/CMakeLists.txt | 27 + .../kwin/source/src/helpers/killer/killer.cpp | 280 + .../killer/org.kde.kwin.killer.desktop.in | 91 + .../helpers/wayland_wrapper/CMakeLists.txt | 23 + .../helpers/wayland_wrapper/kwin_wrapper.cpp | 214 + .../src/helpers/wayland_wrapper/wl-socket.c | 172 + .../src/helpers/wayland_wrapper/wl-socket.h | 39 + .../kde/kwin/source/src/hide_cursor_spy.cpp | 69 + .../kde/kwin/source/src/hide_cursor_spy.h | 31 + .../kde/kwin/source/src/idle_inhibition.cpp | 100 + .../kde/kwin/source/src/idle_inhibition.h | 40 + .../kde/kwin/source/src/idledetector.cpp | 94 + .../kde/kwin/source/src/idledetector.h | 55 + local/recipes/kde/kwin/source/src/input.cpp | 3768 +++++++++++++ local/recipes/kde/kwin/source/src/input.h | 546 ++ .../kde/kwin/source/src/input_event.cpp | 9 + .../recipes/kde/kwin/source/src/input_event.h | 273 + .../kde/kwin/source/src/input_event_spy.cpp | 133 + .../kde/kwin/source/src/input_event_spy.h | 108 + .../kde/kwin/source/src/inputmethod.cpp | 1109 ++++ .../recipes/kde/kwin/source/src/inputmethod.h | 163 + .../source/src/inputpanelv1integration.cpp | 32 + .../kwin/source/src/inputpanelv1integration.h | 26 + .../kwin/source/src/inputpanelv1window.cpp | 259 + .../kde/kwin/source/src/inputpanelv1window.h | 105 + .../kde/kwin/source/src/internalwindow.cpp | 496 ++ .../kde/kwin/source/src/internalwindow.h | 103 + .../kde/kwin/source/src/kcms/CMakeLists.txt | 18 + .../source/src/kcms/common/CMakeLists.txt | 41 + .../kwin/source/src/kcms/common/Messages.sh | 2 + .../source/src/kcms/common/effectsmodel.cpp | 627 +++ .../source/src/kcms/common/effectsmodel.h | 286 + .../src/kcms/common/genericscriptedconfig.cpp | 197 + .../src/kcms/common/genericscriptedconfig.h | 85 + .../kcms/common/genericscriptedconfig.json | 4 + .../source/src/kcms/decoration/CMakeLists.txt | 54 + .../source/src/kcms/decoration/Messages.sh | 2 + .../declarative-plugin/CMakeLists.txt | 27 + .../declarative-plugin/buttonsmodel.cpp | 189 + .../declarative-plugin/buttonsmodel.h | 52 + .../declarative-plugin/previewbridge.cpp | 231 + .../declarative-plugin/previewbridge.h | 149 + .../declarative-plugin/previewbutton.cpp | 149 + .../declarative-plugin/previewbutton.h | 73 + .../declarative-plugin/previewclient.cpp | 458 ++ .../declarative-plugin/previewclient.h | 206 + .../declarative-plugin/previewitem.cpp | 426 ++ .../declarative-plugin/previewitem.h | 90 + .../declarative-plugin/previewsettings.cpp | 275 + .../declarative-plugin/previewsettings.h | 160 + .../decoration/declarative-plugin/types.h | 25 + .../src/kcms/decoration/decorationmodel.cpp | 171 + .../src/kcms/decoration/decorationmodel.h | 53 + .../kwin/source/src/kcms/decoration/kcm.cpp | 284 + .../kde/kwin/source/src/kcms/decoration/kcm.h | 103 + .../kcms/decoration/kcm_kwindecoration.json | 149 + .../decoration/kwin-applywindowdecoration.cpp | 130 + .../decoration/kwindecorationsettings.kcfg | 45 + .../decoration/kwindecorationsettings.kcfgc | 7 + .../src/kcms/decoration/ui/ButtonGroup.qml | 65 + .../source/src/kcms/decoration/ui/Buttons.qml | 264 + .../kcms/decoration/ui/ConfigureTitlebar.qml | 93 + .../source/src/kcms/decoration/ui/Themes.qml | 117 + .../source/src/kcms/decoration/ui/main.qml | 76 + .../kwin/source/src/kcms/decoration/utils.cpp | 107 + .../kwin/source/src/kcms/decoration/utils.h | 27 + .../decoration/window-decorations.knsrc.cmake | 69 + .../source/src/kcms/desktop/CMakeLists.txt | 29 + .../kwin/source/src/kcms/desktop/Messages.sh | 2 + .../src/kcms/desktop/animationsmodel.cpp | 178 + .../source/src/kcms/desktop/animationsmodel.h | 71 + .../source/src/kcms/desktop/desktopsmodel.cpp | 688 +++ .../source/src/kcms/desktop/desktopsmodel.h | 125 + .../desktop/kcm_kwin_virtualdesktops.json | 149 + .../kwin/source/src/kcms/desktop/ui/main.qml | 337 ++ .../src/kcms/desktop/virtualdesktops.cpp | 165 + .../source/src/kcms/desktop/virtualdesktops.h | 55 + .../src/kcms/desktop/virtualdesktopsdata.cpp | 54 + .../src/kcms/desktop/virtualdesktopsdata.h | 40 + .../kcms/desktop/virtualdesktopssettings.kcfg | 29 + .../desktop/virtualdesktopssettings.kcfgc | 6 + .../source/src/kcms/effects/CMakeLists.txt | 27 + .../kwin/source/src/kcms/effects/Messages.sh | 2 + .../src/kcms/effects/desktopeffectsdata.cpp | 39 + .../src/kcms/effects/desktopeffectsdata.h | 31 + .../kcms/effects/effectsfilterproxymodel.cpp | 73 + .../kcms/effects/effectsfilterproxymodel.h | 45 + .../kde/kwin/source/src/kcms/effects/kcm.cpp | 87 + .../kde/kwin/source/src/kcms/effects/kcm.h | 46 + .../src/kcms/effects/kcm_kwin_effects.json | 140 + .../source/src/kcms/effects/kwineffect.knsrc | 65 + .../source/src/kcms/effects/ui/Effect.qml | 117 + .../kwin/source/src/kcms/effects/ui/main.qml | 113 + .../kde/kwin/source/src/kcms/options/AUTHORS | 12 + .../source/src/kcms/options/CMakeLists.txt | 35 + .../kwin/source/src/kcms/options/ChangeLog | 51 + .../kwin/source/src/kcms/options/Messages.sh | 3 + .../kwin/source/src/kcms/options/actions.ui | 553 ++ .../kwin/source/src/kcms/options/advanced.ui | 117 + .../kde/kwin/source/src/kcms/options/focus.ui | 284 + .../src/kcms/options/kcm_kwinoptions.json | 137 + .../kwinoptions_kdeglobals_settings.kcfg | 15 + .../kwinoptions_kdeglobals_settings.kcfgc | 6 + .../kcms/options/kwinoptions_settings.kcfg | 367 ++ .../kcms/options/kwinoptions_settings.kcfgc | 6 + .../kde/kwin/source/src/kcms/options/main.cpp | 214 + .../kde/kwin/source/src/kcms/options/main.h | 75 + .../kwin/source/src/kcms/options/mouse.cpp | 52 + .../kde/kwin/source/src/kcms/options/mouse.h | 69 + .../kde/kwin/source/src/kcms/options/mouse.ui | 709 +++ .../kwin/source/src/kcms/options/moving.ui | 134 + .../kwin/source/src/kcms/options/windows.cpp | 297 ++ .../kwin/source/src/kcms/options/windows.h | 132 + .../kwin/source/src/kcms/rules/CMakeLists.txt | 46 + .../kwin/source/src/kcms/rules/Messages.sh | 2 + .../source/src/kcms/rules/kcm_kwinrules.json | 148 + .../kwin/source/src/kcms/rules/kcmrules.cpp | 495 ++ .../kde/kwin/source/src/kcms/rules/kcmrules.h | 69 + .../source/src/kcms/rules/optionsmodel.cpp | 249 + .../kwin/source/src/kcms/rules/optionsmodel.h | 129 + .../source/src/kcms/rules/rulebookmodel.cpp | 204 + .../source/src/kcms/rules/rulebookmodel.h | 57 + .../kwin/source/src/kcms/rules/ruleitem.cpp | 213 + .../kde/kwin/source/src/kcms/rules/ruleitem.h | 108 + .../kwin/source/src/kcms/rules/rulesmodel.cpp | 993 ++++ .../kwin/source/src/kcms/rules/rulesmodel.h | 120 + .../src/kcms/rules/ui/FileDialogLoader.qml | 46 + .../src/kcms/rules/ui/OptionsComboBox.qml | 122 + .../src/kcms/rules/ui/RuleItemDelegate.qml | 113 + .../source/src/kcms/rules/ui/RulesEditor.qml | 350 ++ .../source/src/kcms/rules/ui/ValueEditor.qml | 231 + .../kwin/source/src/kcms/rules/ui/main.qml | 269 + .../src/kcms/screenedges/CMakeLists.txt | 59 + .../source/src/kcms/screenedges/Messages.sh | 4 + .../kcms/screenedges/kcm_kwinscreenedges.json | 143 + .../kcms/screenedges/kcm_kwintouchscreen.json | 143 + .../src/kcms/screenedges/kwinscreenedge.cpp | 224 + .../src/kcms/screenedges/kwinscreenedge.h | 71 + .../screenedges/kwinscreenedgeconfigform.cpp | 135 + .../screenedges/kwinscreenedgeconfigform.h | 71 + .../kwinscreenedgeeffectsettings.kcfg | 14 + .../kwinscreenedgeeffectsettings.kcfgc | 7 + .../kwinscreenedgescriptsettings.kcfg | 14 + .../kwinscreenedgescriptsettings.kcfgc | 7 + .../screenedges/kwinscreenedgesettings.kcfg | 98 + .../screenedges/kwinscreenedgesettings.kcfgc | 7 + .../kwintouchscreenedgeconfigform.cpp | 33 + .../kwintouchscreenedgeconfigform.h | 38 + .../kwintouchscreenedgeeffectsettings.kcfg | 14 + .../kwintouchscreenedgeeffectsettings.kcfgc | 7 + .../kwintouchscreenscriptsettings.kcfg | 14 + .../kwintouchscreenscriptsettings.kcfgc | 7 + .../screenedges/kwintouchscreensettings.kcfg | 48 + .../screenedges/kwintouchscreensettings.kcfgc | 7 + .../kwin/source/src/kcms/screenedges/main.cpp | 406 ++ .../kwin/source/src/kcms/screenedges/main.h | 68 + .../kwin/source/src/kcms/screenedges/main.ui | 401 ++ .../source/src/kcms/screenedges/monitor.cpp | 288 + .../source/src/kcms/screenedges/monitor.h | 106 + .../kcms/screenedges/screenpreviewwidget.cpp | 161 + .../kcms/screenedges/screenpreviewwidget.h | 45 + .../source/src/kcms/screenedges/touch.cpp | 349 ++ .../kwin/source/src/kcms/screenedges/touch.h | 70 + .../kwin/source/src/kcms/screenedges/touch.ui | 72 + .../source/src/kcms/scripts/CMakeLists.txt | 21 + .../kwin/source/src/kcms/scripts/Messages.sh | 4 + .../src/kcms/scripts/kcm_kwin_scripts.json | 144 + .../source/src/kcms/scripts/kwinscripts.knsrc | 61 + .../src/kcms/scripts/kwinscriptsdata.cpp | 42 + .../source/src/kcms/scripts/kwinscriptsdata.h | 29 + .../kwin/source/src/kcms/scripts/module.cpp | 156 + .../kde/kwin/source/src/kcms/scripts/module.h | 83 + .../kwin/source/src/kcms/scripts/ui/main.qml | 73 + .../source/src/kcms/tabbox/CMakeLists.txt | 58 + .../kwin/source/src/kcms/tabbox/Messages.sh | 4 + .../src/kcms/tabbox/kcm_kwintabbox.json | 143 + .../kcms/tabbox/kwinswitcheffectsettings.kcfg | 17 + .../tabbox/kwinswitcheffectsettings.kcfgc | 6 + .../source/src/kcms/tabbox/kwinswitcher.knsrc | 50 + .../src/kcms/tabbox/kwintabboxconfigform.cpp | 394 ++ .../src/kcms/tabbox/kwintabboxconfigform.h | 111 + .../source/src/kcms/tabbox/kwintabboxdata.cpp | 49 + .../source/src/kcms/tabbox/kwintabboxdata.h | 43 + .../src/kcms/tabbox/kwintabboxsettings.kcfg | 44 + .../src/kcms/tabbox/kwintabboxsettings.kcfgc | 7 + .../source/src/kcms/tabbox/layoutpreview.cpp | 328 ++ .../source/src/kcms/tabbox/layoutpreview.h | 175 + .../kde/kwin/source/src/kcms/tabbox/main.cpp | 318 ++ .../kde/kwin/source/src/kcms/tabbox/main.h | 62 + .../kde/kwin/source/src/kcms/tabbox/main.ui | 823 +++ .../src/kcms/tabbox/shortcutsettings.cpp | 176 + .../source/src/kcms/tabbox/shortcutsettings.h | 42 + .../source/src/kcms/tabbox/thumbnailitem.cpp | 117 + .../source/src/kcms/tabbox/thumbnailitem.h | 54 + .../src/kcms/tabbox/thumbnails/desktop.png | Bin 0 -> 575167 bytes .../src/kcms/tabbox/thumbnails/dolphin.png | Bin 0 -> 106246 bytes .../src/kcms/tabbox/thumbnails/falkon.png | Bin 0 -> 221075 bytes .../src/kcms/tabbox/thumbnails/kmail.png | Bin 0 -> 105097 bytes .../kcms/tabbox/thumbnails/systemsettings.png | Bin 0 -> 139520 bytes .../src/kcms/virtualkeyboard/CMakeLists.txt | 22 + .../src/kcms/virtualkeyboard/Messages.sh | 2 + .../virtualkeyboard/kcm_virtualkeyboard.json | 146 + .../virtualkeyboard/kcmvirtualkeyboard.cpp | 94 + .../kcms/virtualkeyboard/kcmvirtualkeyboard.h | 57 + .../src/kcms/virtualkeyboard/ui/main.qml | 40 + .../virtualkeyboardsettings.kcfg | 10 + .../virtualkeyboardsettings.kcfgc | 7 + .../source/src/kcms/xwayland/CMakeLists.txt | 23 + .../kwin/source/src/kcms/xwayland/Messages.sh | 2 + .../src/kcms/xwayland/kcm_kwinxwayland.json | 139 + .../src/kcms/xwayland/kcmkwinxwayland.cpp | 54 + .../src/kcms/xwayland/kcmkwinxwayland.h | 42 + .../kcms/xwayland/kwinxwaylandsettings.kcfg | 24 + .../kcms/xwayland/kwinxwaylandsettings.kcfgc | 9 + .../kwin/source/src/kcms/xwayland/ui/main.qml | 286 + .../kde/kwin/source/src/keyboard_input.cpp | 348 ++ .../kde/kwin/source/src/keyboard_input.h | 94 + .../kde/kwin/source/src/keyboard_layout.cpp | 268 + .../kde/kwin/source/src/keyboard_layout.h | 110 + .../source/src/keyboard_layout_switching.cpp | 331 ++ .../source/src/keyboard_layout_switching.h | 143 + .../kde/kwin/source/src/keyboard_repeat.cpp | 64 + .../kde/kwin/source/src/keyboard_repeat.h | 41 + .../kde/kwin/source/src/killprompt.cpp | 112 + .../recipes/kde/kwin/source/src/killprompt.h | 45 + .../kde/kwin/source/src/killwindow.cpp | 43 + .../recipes/kde/kwin/source/src/killwindow.h | 26 + .../kwin/source/src/kscreenintegration.cpp | 264 + .../kde/kwin/source/src/kscreenintegration.h | 24 + local/recipes/kde/kwin/source/src/kwin.kcfg | 353 ++ .../recipes/kde/kwin/source/src/kwin.notifyrc | 287 + local/recipes/kde/kwin/source/src/layers.cpp | 724 +++ .../source/src/layershellv1integration.cpp | 223 + .../kwin/source/src/layershellv1integration.h | 30 + .../kwin/source/src/layershellv1window.cpp | 407 ++ .../kde/kwin/source/src/layershellv1window.h | 85 + .../kde/kwin/source/src/lidswitchtracker.cpp | 39 + .../kde/kwin/source/src/lidswitchtracker.h | 34 + local/recipes/kde/kwin/source/src/main.cpp | 630 +++ local/recipes/kde/kwin/source/src/main.h | 393 ++ .../kde/kwin/source/src/main_wayland.cpp | 643 +++ .../kde/kwin/source/src/main_wayland.h | 97 + .../kde/kwin/source/src/mousebuttons.cpp | 50 + .../kde/kwin/source/src/mousebuttons.h | 17 + local/recipes/kde/kwin/source/src/netinfo.cpp | 332 ++ local/recipes/kde/kwin/source/src/netinfo.h | 92 + .../kwin/source/src/onscreennotification.cpp | 231 + .../kwin/source/src/onscreennotification.h | 79 + ...tract_opengl_context_attribute_builder.cpp | 30 + ...bstract_opengl_context_attribute_builder.h | 132 + .../source/src/opengl/colormanagement.glsl | 193 + .../opengl/egl_context_attribute_builder.cpp | 79 + .../opengl/egl_context_attribute_builder.h | 28 + .../kde/kwin/source/src/opengl/eglcontext.cpp | 682 +++ .../kde/kwin/source/src/opengl/eglcontext.h | 149 + .../kde/kwin/source/src/opengl/egldisplay.cpp | 367 ++ .../kde/kwin/source/src/opengl/egldisplay.h | 86 + .../source/src/opengl/eglimagetexture.cpp | 49 + .../kwin/source/src/opengl/eglimagetexture.h | 34 + .../kwin/source/src/opengl/eglnativefence.cpp | 84 + .../kwin/source/src/opengl/eglnativefence.h | 41 + .../kwin/source/src/opengl/eglswapchain.cpp | 183 + .../kde/kwin/source/src/opengl/eglswapchain.h | 80 + .../kde/kwin/source/src/opengl/eglutils_p.h | 56 + .../kwin/source/src/opengl/glframebuffer.cpp | 310 ++ .../kwin/source/src/opengl/glframebuffer.h | 133 + .../kde/kwin/source/src/opengl/gllut.cpp | 78 + .../kde/kwin/source/src/opengl/gllut.h | 40 + .../kde/kwin/source/src/opengl/gllut3D.cpp | 94 + .../kde/kwin/source/src/opengl/gllut3D.h | 44 + .../kde/kwin/source/src/opengl/glplatform.cpp | 1131 ++++ .../kde/kwin/source/src/opengl/glplatform.h | 335 ++ .../source/src/opengl/glrendertimequery.cpp | 87 + .../source/src/opengl/glrendertimequery.h | 54 + .../kde/kwin/source/src/opengl/glshader.cpp | 511 ++ .../kde/kwin/source/src/opengl/glshader.h | 177 + .../source/src/opengl/glshadermanager.cpp | 415 ++ .../kwin/source/src/opengl/glshadermanager.h | 232 + .../kde/kwin/source/src/opengl/gltexture.cpp | 586 ++ .../kde/kwin/source/src/opengl/gltexture.h | 136 + .../kde/kwin/source/src/opengl/gltexture_p.h | 61 + .../kde/kwin/source/src/opengl/glutils.cpp | 61 + .../kde/kwin/source/src/opengl/glutils.h | 33 + .../kwin/source/src/opengl/glvertexbuffer.cpp | 595 +++ .../kwin/source/src/opengl/glvertexbuffer.h | 288 + .../kwin/source/src/opengl/glvertexbuffer_p.h | 35 + .../kde/kwin/source/src/opengl/icc.frag | 62 + .../kde/kwin/source/src/opengl/icc_core.frag | 65 + .../kde/kwin/source/src/opengl/icc_shader.cpp | 261 + .../kde/kwin/source/src/opengl/icc_shader.h | 62 + .../kwin/source/src/opengl/saturation.glsl | 9 + local/recipes/kde/kwin/source/src/options.cpp | 906 ++++ local/recipes/kde/kwin/source/src/options.h | 945 ++++ .../src/org.freedesktop.DBus.Properties.xml | 27 + .../kwin/source/src/org.kde.KWin.Plugins.xml | 32 + .../kwin/source/src/org.kde.KWin.Session.xml | 30 + .../org.kde.KWin.VirtualDesktopManager.xml | 50 + .../kde/kwin/source/src/org.kde.KWin.xml | 48 + .../kde/kwin/source/src/org.kde.kappmenu.xml | 28 + .../source/src/org.kde.kwin.Compositing.xml | 15 + .../kwin/source/src/org.kde.kwin.Effects.xml | 43 + local/recipes/kde/kwin/source/src/osd.cpp | 67 + local/recipes/kde/kwin/source/src/osd.h | 28 + local/recipes/kde/kwin/source/src/outline.cpp | 180 + local/recipes/kde/kwin/source/src/outline.h | 134 + .../source/src/outputconfigurationstore.cpp | 1554 ++++++ .../source/src/outputconfigurationstore.h | 130 + .../src/placeholderinputeventfilter.cpp | 57 + .../source/src/placeholderinputeventfilter.h | 29 + .../kde/kwin/source/src/placeholderoutput.cpp | 57 + .../kde/kwin/source/src/placeholderoutput.h | 30 + .../recipes/kde/kwin/source/src/placement.cpp | 901 ++++ local/recipes/kde/kwin/source/src/placement.h | 49 + .../kde/kwin/source/src/placementtracker.cpp | 216 + .../kde/kwin/source/src/placementtracker.h | 70 + local/recipes/kde/kwin/source/src/plugin.cpp | 18 + local/recipes/kde/kwin/source/src/plugin.h | 49 + .../kde/kwin/source/src/pluginmanager.cpp | 135 + .../kde/kwin/source/src/pluginmanager.h | 45 + .../kwin/source/src/plugins/CMakeLists.txt | 137 + .../src/plugins/blendchanges/CMakeLists.txt | 13 + .../src/plugins/blendchanges/blendchanges.cpp | 105 + .../src/plugins/blendchanges/blendchanges.h | 57 + .../source/src/plugins/blendchanges/main.cpp | 18 + .../src/plugins/blendchanges/metadata.json | 104 + .../source/src/plugins/blur/CMakeLists.txt | 39 + .../kde/kwin/source/src/plugins/blur/blur.cpp | 979 ++++ .../kde/kwin/source/src/plugins/blur/blur.h | 198 + .../kwin/source/src/plugins/blur/blur.kcfg | 18 + .../kde/kwin/source/src/plugins/blur/blur.qrc | 18 + .../source/src/plugins/blur/blur_config.cpp | 47 + .../source/src/plugins/blur/blur_config.h | 29 + .../source/src/plugins/blur/blur_config.ui | 183 + .../source/src/plugins/blur/blurconfig.kcfgc | 5 + .../kde/kwin/source/src/plugins/blur/main.cpp | 20 + .../source/src/plugins/blur/metadata.json | 105 + .../src/plugins/blur/shaders/downsample.frag | 16 + .../plugins/blur/shaders/downsample_core.frag | 20 + .../src/plugins/blur/shaders/noise.frag | 11 + .../src/plugins/blur/shaders/noise_core.frag | 15 + .../src/plugins/blur/shaders/upsample.frag | 19 + .../plugins/blur/shaders/upsample_core.frag | 23 + .../src/plugins/blur/shaders/vertex.vert | 12 + .../src/plugins/blur/shaders/vertex_core.vert | 14 + .../src/plugins/bouncekeys/CMakeLists.txt | 18 + .../src/plugins/bouncekeys/bouncekeys.cpp | 66 + .../src/plugins/bouncekeys/bouncekeys.h | 34 + .../source/src/plugins/bouncekeys/main.cpp | 23 + .../src/plugins/bouncekeys/metadata.json | 5 + .../src/plugins/buttonrebinds/CMakeLists.txt | 17 + .../buttonrebinds/buttonrebindsfilter.cpp | 612 +++ .../buttonrebinds/buttonrebindsfilter.h | 111 + .../source/src/plugins/buttonrebinds/main.cpp | 25 + .../src/plugins/buttonrebinds/metadata.json | 5 + .../colorblindnesscorrection/CMakeLists.txt | 19 + .../colorblindnesscorrection.cpp | 192 + .../colorblindnesscorrection.h | 64 + .../colorblindnesscorrection.qrc | 8 + .../colorblindnesscorrectionconfig.kcfg | 19 + .../colorblindnesscorrectionconfig.kcfgc | 7 + .../plugins/colorblindnesscorrection/main.cpp | 16 + .../colorblindnesscorrection/metadata.json | 95 + .../metadata.json.license | 2 + .../colorblindnesscorrection/shaders/README | 8 + .../shaders/colorblindnesscorrection.frag | 40 + .../colorblindnesscorrection_core.frag | 42 + .../src/plugins/colorpicker/CMakeLists.txt | 17 + .../src/plugins/colorpicker/colorpicker.cpp | 176 + .../src/plugins/colorpicker/colorpicker.h | 49 + .../source/src/plugins/colorpicker/main.cpp | 18 + .../src/plugins/colorpicker/metadata.json | 104 + .../plugins/desktopchangeosd/CMakeLists.txt | 1 + .../package/contents/ui/main.qml | 24 + .../package/contents/ui/osd.qml | 286 + .../desktopchangeosd/package/metadata.json | 153 + .../src/plugins/dialogparent/CMakeLists.txt | 1 + .../package/contents/code/main.js | 158 + .../dialogparent/package/metadata.json | 156 + .../src/plugins/diminactive/CMakeLists.txt | 36 + .../src/plugins/diminactive/diminactive.cpp | 423 ++ .../src/plugins/diminactive/diminactive.h | 131 + .../src/plugins/diminactive/diminactive.kcfg | 27 + .../diminactive/diminactive_config.cpp | 52 + .../plugins/diminactive/diminactive_config.h | 34 + .../plugins/diminactive/diminactive_config.ui | 86 + .../diminactive/diminactiveconfig.kcfgc | 5 + .../source/src/plugins/diminactive/main.cpp | 17 + .../src/plugins/diminactive/metadata.json | 102 + .../src/plugins/dimscreen/CMakeLists.txt | 1 + .../dimscreen/package/contents/code/main.js | 237 + .../plugins/dimscreen/package/metadata.json | 156 + .../source/src/plugins/eis/CMakeLists.txt | 37 + .../source/src/plugins/eis/eisbackend.cpp | 198 + .../kwin/source/src/plugins/eis/eisbackend.h | 54 + .../source/src/plugins/eis/eiscontext.cpp | 367 ++ .../kwin/source/src/plugins/eis/eiscontext.h | 69 + .../kwin/source/src/plugins/eis/eisdevice.cpp | 111 + .../kwin/source/src/plugins/eis/eisdevice.h | 52 + .../src/plugins/eis/eisinputcapture.cpp | 327 ++ .../source/src/plugins/eis/eisinputcapture.h | 72 + .../src/plugins/eis/eisinputcapturefilter.cpp | 246 + .../src/plugins/eis/eisinputcapturefilter.h | 57 + .../plugins/eis/eisinputcapturemanager.cpp | 221 + .../src/plugins/eis/eisinputcapturemanager.h | 62 + .../kwin/source/src/plugins/eis/eisplugin.cpp | 23 + .../kwin/source/src/plugins/eis/eisplugin.h | 25 + .../kde/kwin/source/src/plugins/eis/main.cpp | 24 + .../kwin/source/src/plugins/eis/metadata.json | 5 + .../src/plugins/eyeonscreen/CMakeLists.txt | 1 + .../eyeonscreen/package/contents/code/main.js | 97 + .../plugins/eyeonscreen/package/metadata.json | 157 + .../source/src/plugins/fade/CMakeLists.txt | 1 + .../fade/package/contents/code/main.js | 120 + .../fade/package/contents/config/main.xml | 20 + .../src/plugins/fade/package/metadata.json | 144 + .../src/plugins/fadedesktop/CMakeLists.txt | 1 + .../fadedesktop/package/contents/code/main.js | 124 + .../plugins/fadedesktop/package/metadata.json | 146 + .../src/plugins/fadingpopups/CMakeLists.txt | 1 + .../package/contents/code/main.js | 141 + .../fadingpopups/package/metadata.json | 131 + .../src/plugins/fallapart/CMakeLists.txt | 19 + .../src/plugins/fallapart/fallapart.cpp | 222 + .../source/src/plugins/fallapart/fallapart.h | 63 + .../src/plugins/fallapart/fallapart.kcfg | 14 + .../plugins/fallapart/fallapartconfig.kcfgc | 5 + .../source/src/plugins/fallapart/main.cpp | 18 + .../src/plugins/fallapart/metadata.json | 101 + .../src/plugins/frozenapp/CMakeLists.txt | 1 + .../frozenapp/package/contents/code/main.js | 123 + .../plugins/frozenapp/package/metadata.json | 158 + .../src/plugins/fullscreen/CMakeLists.txt | 1 + .../fullscreen/package/contents/code/main.js | 128 + .../plugins/fullscreen/package/metadata.json | 131 + .../source/src/plugins/glide/CMakeLists.txt | 36 + .../kwin/source/src/plugins/glide/glide.cpp | 308 ++ .../kde/kwin/source/src/plugins/glide/glide.h | 155 + .../kwin/source/src/plugins/glide/glide.kcfg | 40 + .../source/src/plugins/glide/glide_config.cpp | 47 + .../source/src/plugins/glide/glide_config.h | 30 + .../source/src/plugins/glide/glide_config.ui | 260 + .../src/plugins/glide/glideconfig.kcfgc | 5 + .../kwin/source/src/plugins/glide/main.cpp | 18 + .../source/src/plugins/glide/metadata.json | 103 + .../src/plugins/hidecursor/CMakeLists.txt | 34 + .../src/plugins/hidecursor/hidecursor.cpp | 121 + .../src/plugins/hidecursor/hidecursor.h | 52 + .../plugins/hidecursor/hidecursor_config.cpp | 81 + .../plugins/hidecursor/hidecursor_config.h | 48 + .../plugins/hidecursor/hidecursor_config.ui | 32 + .../plugins/hidecursor/hidecursorconfig.kcfg | 15 + .../plugins/hidecursor/hidecursorconfig.kcfgc | 8 + .../source/src/plugins/hidecursor/main.cpp | 17 + .../src/plugins/hidecursor/metadata.json | 93 + .../plugins/highlightwindow/CMakeLists.txt | 15 + .../highlightwindow/highlightwindow.cpp | 206 + .../plugins/highlightwindow/highlightwindow.h | 57 + .../src/plugins/highlightwindow/main.cpp | 17 + .../src/plugins/highlightwindow/metadata.json | 104 + .../src/plugins/idletime/CMakeLists.txt | 10 + .../source/src/plugins/idletime/kwin.json | 3 + .../source/src/plugins/idletime/poller.cpp | 91 + .../kwin/source/src/plugins/idletime/poller.h | 47 + .../source/src/plugins/invert/CMakeLists.txt | 16 + .../kwin/source/src/plugins/invert/invert.cpp | 167 + .../kwin/source/src/plugins/invert/invert.h | 63 + .../kwin/source/src/plugins/invert/invert.qrc | 6 + .../kwin/source/src/plugins/invert/main.cpp | 18 + .../source/src/plugins/invert/metadata.json | 95 + .../src/plugins/invert/shaders/invert.frag | 24 + .../plugins/invert/shaders/invert_core.frag | 28 + .../plugins/keynotification/CMakeLists.txt | 15 + .../keynotification/keynotification.cpp | 127 + .../plugins/keynotification/keynotification.h | 35 + .../src/plugins/keynotification/main.cpp | 24 + .../src/plugins/keynotification/metadata.json | 5 + .../src/plugins/kglobalaccel/CMakeLists.txt | 3 + .../kglobalaccel/kglobalaccel_plugin.cpp | 56 + .../kglobalaccel/kglobalaccel_plugin.h | 34 + .../source/src/plugins/kglobalaccel/kwin.json | 3 + .../src/plugins/kpackage/CMakeLists.txt | 13 + .../src/plugins/kpackage/aurorae/aurorae.cpp | 48 + .../src/plugins/kpackage/aurorae/aurorae.json | 3 + .../kpackage/decoration/decoration.cpp | 34 + .../kpackage/decoration/decoration.json | 3 + .../src/plugins/kpackage/effect/effect.cpp | 50 + .../src/plugins/kpackage/effect/effect.json | 3 + .../src/plugins/kpackage/scripts/scripts.cpp | 47 + .../src/plugins/kpackage/scripts/scripts.json | 3 + .../windowswitcher/windowswitcher.cpp | 34 + .../windowswitcher/windowswitcher.json | 3 + .../krunner-integration/CMakeLists.txt | 11 + .../plugins/krunner-integration/dbusutils_p.h | 134 + .../kwin-runner-windows.desktop | 152 + .../src/plugins/krunner-integration/main.cpp | 30 + .../plugins/krunner-integration/metadata.json | 5 + .../krunner-integration/org.kde.krunner1.xml | 55 + .../windowsrunnerinterface.cpp | 349 ++ .../windowsrunnerinterface.h | 60 + .../source/src/plugins/kscreen/CMakeLists.txt | 16 + .../source/src/plugins/kscreen/kscreen.cpp | 168 + .../kwin/source/src/plugins/kscreen/kscreen.h | 60 + .../source/src/plugins/kscreen/kscreen.kcfg | 12 + .../src/plugins/kscreen/kscreenconfig.kcfgc | 5 + .../kwin/source/src/plugins/kscreen/main.cpp | 17 + .../source/src/plugins/kscreen/metadata.json | 106 + .../source/src/plugins/login/CMakeLists.txt | 1 + .../login/package/contents/code/main.js | 69 + .../login/package/contents/config/main.xml | 12 + .../login/package/contents/ui/config.ui | 38 + .../src/plugins/login/package/metadata.json | 145 + .../source/src/plugins/logout/CMakeLists.txt | 1 + .../logout/package/contents/code/main.js | 61 + .../src/plugins/logout/package/metadata.json | 144 + .../src/plugins/magiclamp/CMakeLists.txt | 36 + .../src/plugins/magiclamp/magiclamp.cpp | 440 ++ .../source/src/plugins/magiclamp/magiclamp.h | 66 + .../src/plugins/magiclamp/magiclamp.kcfg | 12 + .../plugins/magiclamp/magiclamp_config.cpp | 50 + .../src/plugins/magiclamp/magiclamp_config.h | 31 + .../src/plugins/magiclamp/magiclamp_config.ui | 53 + .../plugins/magiclamp/magiclampconfig.kcfgc | 5 + .../source/src/plugins/magiclamp/main.cpp | 18 + .../src/plugins/magiclamp/metadata.json | 105 + .../src/plugins/magnifier/CMakeLists.txt | 20 + .../src/plugins/magnifier/magnifier.cpp | 353 ++ .../source/src/plugins/magnifier/magnifier.h | 76 + .../src/plugins/magnifier/magnifier.kcfg | 28 + .../plugins/magnifier/magnifierconfig.kcfgc | 5 + .../source/src/plugins/magnifier/main.cpp | 18 + .../src/plugins/magnifier/metadata.json | 96 + .../src/plugins/maximize/CMakeLists.txt | 1 + .../maximize/package/contents/code/main.js | 127 + .../plugins/maximize/package/metadata.json | 131 + .../src/plugins/minimizeall/CMakeLists.txt | 1 + .../minimizeall/package/contents/code/main.js | 116 + .../plugins/minimizeall/package/metadata.json | 155 + .../src/plugins/mouseclick/CMakeLists.txt | 40 + .../source/src/plugins/mouseclick/main.cpp | 17 + .../src/plugins/mouseclick/metadata.json | 94 + .../src/plugins/mouseclick/mouseclick.cpp | 421 ++ .../src/plugins/mouseclick/mouseclick.h | 165 + .../src/plugins/mouseclick/mouseclick.kcfg | 34 + .../plugins/mouseclick/mouseclick_config.cpp | 68 + .../plugins/mouseclick/mouseclick_config.h | 34 + .../plugins/mouseclick/mouseclick_config.ui | 282 + .../plugins/mouseclick/mouseclickconfig.kcfgc | 5 + .../src/plugins/mousemark/CMakeLists.txt | 46 + .../source/src/plugins/mousemark/main.cpp | 18 + .../src/plugins/mousemark/metadata.json | 95 + .../src/plugins/mousemark/mousemark.cpp | 275 + .../source/src/plugins/mousemark/mousemark.h | 68 + .../src/plugins/mousemark/mousemark.kcfg | 40 + .../plugins/mousemark/mousemark_config.cpp | 95 + .../src/plugins/mousemark/mousemark_config.h | 38 + .../src/plugins/mousemark/mousemark_config.ui | 242 + .../plugins/mousemark/mousemarkconfig.kcfgc | 5 + .../src/plugins/nightlight/CMakeLists.txt | 36 + .../source/src/plugins/nightlight/constants.h | 22 + .../source/src/plugins/nightlight/main.cpp | 30 + .../src/plugins/nightlight/metadata.json | 5 + .../nightlight/nightlightdbusinterface.cpp | 237 + .../nightlight/nightlightdbusinterface.h | 90 + .../plugins/nightlight/nightlightmanager.cpp | 581 ++ .../plugins/nightlight/nightlightmanager.h | 292 + .../nightlight/nightlightsettings.kcfg | 25 + .../nightlight/nightlightsettings.kcfgc | 8 + .../nightlight/org.kde.KWin.NightLight.xml | 123 + .../src/plugins/outputlocator/CMakeLists.txt | 20 + .../source/src/plugins/outputlocator/main.cpp | 14 + .../src/plugins/outputlocator/metadata.json | 147 + .../plugins/outputlocator/outputlocator.cpp | 117 + .../src/plugins/outputlocator/outputlocator.h | 36 + .../plugins/outputlocator/qml/OutputLabel.qml | 47 + .../src/plugins/overview/CMakeLists.txt | 37 + .../src/plugins/overview/kcm/CMakeLists.txt | 17 + .../overview/kcm/overvieweffectkcm.cpp | 90 + .../plugins/overview/kcm/overvieweffectkcm.h | 31 + .../plugins/overview/kcm/overvieweffectkcm.ui | 86 + .../kwin/source/src/plugins/overview/main.cpp | 18 + .../source/src/plugins/overview/metadata.json | 94 + .../src/plugins/overview/overviewconfig.kcfg | 30 + .../src/plugins/overview/overviewconfig.kcfgc | 10 + .../src/plugins/overview/overvieweffect.cpp | 344 ++ .../src/plugins/overview/overvieweffect.h | 101 + .../src/plugins/overview/qml/DesktopBar.qml | 294 + .../src/plugins/overview/qml/DesktopView.qml | 34 + .../source/src/plugins/private/CMakeLists.txt | 27 + .../source/src/plugins/private/expoarea.cpp | 87 + .../source/src/plugins/private/expoarea.h | 48 + .../source/src/plugins/private/expolayout.cpp | 849 +++ .../source/src/plugins/private/expolayout.h | 348 ++ .../source/src/plugins/private/plugin.cpp | 18 + .../kwin/source/src/plugins/private/plugin.h | 18 + .../src/plugins/private/qml/WindowHeap.qml | 389 ++ .../private/qml/WindowHeapDelegate.qml | 392 ++ .../source/src/plugins/qpa/CMakeLists.txt | 30 + .../source/src/plugins/qpa/backingstore.cpp | 112 + .../source/src/plugins/qpa/backingstore.h | 42 + .../kwin/source/src/plugins/qpa/clipboard.cpp | 207 + .../kwin/source/src/plugins/qpa/clipboard.h | 69 + .../source/src/plugins/qpa/eglhelpers.cpp | 126 + .../kwin/source/src/plugins/qpa/eglhelpers.h | 30 + .../src/plugins/qpa/eglplatformcontext.cpp | 268 + .../src/plugins/qpa/eglplatformcontext.h | 80 + .../source/src/plugins/qpa/integration.cpp | 255 + .../kwin/source/src/plugins/qpa/integration.h | 71 + .../kde/kwin/source/src/plugins/qpa/kwin.json | 3 + .../kde/kwin/source/src/plugins/qpa/main.cpp | 36 + .../src/plugins/qpa/offscreensurface.cpp | 39 + .../source/src/plugins/qpa/offscreensurface.h | 37 + .../source/src/plugins/qpa/platformcursor.cpp | 40 + .../source/src/plugins/qpa/platformcursor.h | 29 + .../kwin/source/src/plugins/qpa/screen.cpp | 122 + .../kde/kwin/source/src/plugins/qpa/screen.h | 58 + .../kwin/source/src/plugins/qpa/swapchain.cpp | 60 + .../kwin/source/src/plugins/qpa/swapchain.h | 37 + .../kwin/source/src/plugins/qpa/window.cpp | 176 + .../kde/kwin/source/src/plugins/qpa/window.h | 60 + .../source/src/plugins/scale/CMakeLists.txt | 1 + .../scale/package/contents/code/main.js | 175 + .../scale/package/contents/config/main.xml | 18 + .../scale/package/contents/ui/config.ui | 93 + .../src/plugins/scale/package/metadata.json | 147 + .../src/plugins/screencast/CMakeLists.txt | 23 + .../source/src/plugins/screencast/main.cpp | 30 + .../src/plugins/screencast/metadata.json | 5 + .../screencast/outputscreencastsource.cpp | 173 + .../screencast/outputscreencastsource.h | 57 + .../src/plugins/screencast/pipewirecore.cpp | 108 + .../src/plugins/screencast/pipewirecore.h | 50 + .../screencast/regionscreencastsource.cpp | 183 + .../screencast/regionscreencastsource.h | 61 + .../plugins/screencast/screencastbuffer.cpp | 149 + .../src/plugins/screencast/screencastbuffer.h | 58 + .../plugins/screencast/screencastmanager.cpp | 204 + .../plugins/screencast/screencastmanager.h | 56 + .../plugins/screencast/screencastsource.cpp | 18 + .../src/plugins/screencast/screencastsource.h | 52 + .../plugins/screencast/screencaststream.cpp | 909 ++++ .../src/plugins/screencast/screencaststream.h | 147 + .../src/plugins/screencast/screencastutils.h | 111 + .../screencast/windowscreencastsource.cpp | 215 + .../screencast/windowscreencastsource.h | 55 + .../src/plugins/screenedge/CMakeLists.txt | 15 + .../source/src/plugins/screenedge/main.cpp | 17 + .../src/plugins/screenedge/metadata.json | 86 + .../plugins/screenedge/screenedgeeffect.cpp | 213 + .../src/plugins/screenedge/screenedgeeffect.h | 55 + .../src/plugins/screenshot/CMakeLists.txt | 25 + .../source/src/plugins/screenshot/main.cpp | 32 + .../src/plugins/screenshot/metadata.json | 5 + .../screenshot/org.kde.KWin.ScreenShot2.xml | 373 ++ .../src/plugins/screenshot/screenshot.cpp | 280 + .../src/plugins/screenshot/screenshot.h | 54 + .../screenshot/screenshotdbusinterface2.cpp | 513 ++ .../screenshot/screenshotdbusinterface2.h | 70 + .../plugins/screentransform/CMakeLists.txt | 13 + .../src/plugins/screentransform/main.cpp | 18 + .../src/plugins/screentransform/metadata.json | 104 + .../screentransform/screentransform.cpp | 277 + .../plugins/screentransform/screentransform.h | 71 + .../screentransform/screentransform.qrc | 9 + .../screentransform/shaders/crossfade.frag | 12 + .../screentransform/shaders/crossfade.vert | 12 + .../shaders/crossfade_core.frag | 16 + .../shaders/crossfade_core.vert | 14 + .../src/plugins/sessionquit/CMakeLists.txt | 1 + .../sessionquit/package/contents/code/main.js | 32 + .../plugins/sessionquit/package/metadata.json | 155 + .../src/plugins/shakecursor/CMakeLists.txt | 17 + .../source/src/plugins/shakecursor/main.cpp | 18 + .../src/plugins/shakecursor/metadata.json | 95 + .../src/plugins/shakecursor/shakecursor.cpp | 162 + .../src/plugins/shakecursor/shakecursor.h | 71 + .../shakecursor/shakecursorconfig.kcfg | 21 + .../shakecursor/shakecursorconfig.kcfgc | 8 + .../src/plugins/shakecursor/shakedetector.cpp | 116 + .../src/plugins/shakecursor/shakedetector.h | 52 + .../source/src/plugins/sheet/CMakeLists.txt | 16 + .../kwin/source/src/plugins/sheet/main.cpp | 18 + .../source/src/plugins/sheet/metadata.json | 100 + .../kwin/source/src/plugins/sheet/sheet.cpp | 212 + .../kde/kwin/source/src/plugins/sheet/sheet.h | 75 + .../kwin/source/src/plugins/sheet/sheet.kcfg | 12 + .../src/plugins/sheet/sheetconfig.kcfgc | 5 + .../plugins/showcompositing/CMakeLists.txt | 24 + .../src/plugins/showcompositing/main.cpp | 18 + .../src/plugins/showcompositing/metadata.json | 94 + .../showcompositing/showcompositing.cpp | 55 + .../plugins/showcompositing/showcompositing.h | 35 + .../source/src/plugins/showfps/CMakeLists.txt | 26 + .../kwin/source/src/plugins/showfps/main.cpp | 18 + .../source/src/plugins/showfps/metadata.json | 104 + .../src/plugins/showfps/showfpseffect.cpp | 130 + .../src/plugins/showfps/showfpseffect.h | 64 + .../src/plugins/showpaint/CMakeLists.txt | 12 + .../source/src/plugins/showpaint/main.cpp | 17 + .../src/plugins/showpaint/metadata.json | 95 + .../src/plugins/showpaint/showpaint.cpp | 96 + .../source/src/plugins/showpaint/showpaint.h | 35 + .../source/src/plugins/slide/CMakeLists.txt | 37 + .../kwin/source/src/plugins/slide/main.cpp | 18 + .../source/src/plugins/slide/metadata.json | 90 + .../kwin/source/src/plugins/slide/slide.cpp | 555 ++ .../kde/kwin/source/src/plugins/slide/slide.h | 163 + .../kwin/source/src/plugins/slide/slide.kcfg | 19 + .../source/src/plugins/slide/slide_config.cpp | 51 + .../source/src/plugins/slide/slide_config.h | 32 + .../source/src/plugins/slide/slide_config.ui | 93 + .../src/plugins/slide/slideconfig.kcfgc | 5 + .../source/src/plugins/slide/springmotion.cpp | 164 + .../source/src/plugins/slide/springmotion.h | 103 + .../src/plugins/slideback/CMakeLists.txt | 12 + .../source/src/plugins/slideback/main.cpp | 17 + .../src/plugins/slideback/metadata.json | 99 + .../src/plugins/slideback/motionmanager.cpp | 284 + .../src/plugins/slideback/motionmanager.h | 361 ++ .../src/plugins/slideback/slideback.cpp | 353 ++ .../source/src/plugins/slideback/slideback.h | 66 + .../src/plugins/slidingpopups/CMakeLists.txt | 19 + .../source/src/plugins/slidingpopups/main.cpp | 18 + .../src/plugins/slidingpopups/metadata.json | 75 + .../plugins/slidingpopups/slidingpopups.cpp | 535 ++ .../src/plugins/slidingpopups/slidingpopups.h | 121 + .../plugins/slidingpopups/slidingpopups.kcfg | 17 + .../slidingpopups/slidingpopupsconfig.kcfgc | 5 + .../source/src/plugins/squash/CMakeLists.txt | 1 + .../squash/package/contents/code/main.js | 181 + .../src/plugins/squash/package/metadata.json | 155 + .../plugins/startupfeedback/CMakeLists.txt | 17 + .../src/plugins/startupfeedback/main.cpp | 18 + .../src/plugins/startupfeedback/metadata.json | 104 + .../shaders/blinking-startup.frag | 16 + .../shaders/blinking-startup_core.frag | 20 + .../startupfeedback/startupfeedback.cpp | 482 ++ .../plugins/startupfeedback/startupfeedback.h | 104 + .../startupfeedback/startupfeedback.qrc | 7 + .../src/plugins/stickykeys/CMakeLists.txt | 21 + .../source/src/plugins/stickykeys/main.cpp | 24 + .../src/plugins/stickykeys/metadata.json | 5 + .../src/plugins/stickykeys/stickykeys.cpp | 227 + .../src/plugins/stickykeys/stickykeys.h | 41 + .../src/plugins/strip-effect-metadata.py | 27 + .../synchronizeskipswitcher/CMakeLists.txt | 1 + .../package/contents/code/main.js | 22 + .../package/metadata.json | 151 + .../src/plugins/systembell/CMakeLists.txt | 14 + .../source/src/plugins/systembell/main.cpp | 18 + .../src/plugins/systembell/metadata.json | 94 + .../src/plugins/systembell/shaders/color.frag | 6 + .../systembell/shaders/color_core.frag | 6 + .../plugins/systembell/shaders/invert.frag | 24 + .../systembell/shaders/invert_core.frag | 28 + .../src/plugins/systembell/systembell.cpp | 310 ++ .../src/plugins/systembell/systembell.h | 92 + .../src/plugins/systembell/systembell.qrc | 8 + .../src/plugins/thumbnailaside/CMakeLists.txt | 39 + .../src/plugins/thumbnailaside/main.cpp | 17 + .../src/plugins/thumbnailaside/metadata.json | 94 + .../plugins/thumbnailaside/thumbnailaside.cpp | 209 + .../plugins/thumbnailaside/thumbnailaside.h | 85 + .../thumbnailaside/thumbnailaside.kcfg | 21 + .../thumbnailaside/thumbnailaside_config.cpp | 72 + .../thumbnailaside/thumbnailaside_config.h | 33 + .../thumbnailaside/thumbnailaside_config.ui | 138 + .../thumbnailaside/thumbnailasideconfig.kcfgc | 5 + .../src/plugins/tileseditor/CMakeLists.txt | 35 + .../plugins/tileseditor/kcm/CMakeLists.txt | 16 + .../tileseditor/kcm/tileseditoreffectkcm.cpp | 67 + .../tileseditor/kcm/tileseditoreffectkcm.h | 31 + .../tileseditor/kcm/tileseditoreffectkcm.ui | 36 + .../source/src/plugins/tileseditor/main.cpp | 18 + .../src/plugins/tileseditor/metadata.json | 97 + .../plugins/tileseditor/qml/ResizeCorner.qml | 91 + .../plugins/tileseditor/qml/ResizeHandle.qml | 67 + .../plugins/tileseditor/qml/TileDelegate.qml | 214 + .../src/plugins/tileseditor/qml/layouts.svg | 133 + .../plugins/tileseditor/tileseditoreffect.cpp | 120 + .../plugins/tileseditor/tileseditoreffect.h | 53 + .../src/plugins/touchpoints/CMakeLists.txt | 14 + .../source/src/plugins/touchpoints/main.cpp | 17 + .../src/plugins/touchpoints/metadata.json | 101 + .../src/plugins/touchpoints/touchpoints.cpp | 251 + .../src/plugins/touchpoints/touchpoints.h | 87 + .../src/plugins/trackmouse/CMakeLists.txt | 41 + .../src/plugins/trackmouse/data/tm_inner.png | Bin 0 -> 584 bytes .../src/plugins/trackmouse/data/tm_outer.png | Bin 0 -> 728 bytes .../source/src/plugins/trackmouse/main.cpp | 17 + .../src/plugins/trackmouse/metadata.json | 94 + .../src/plugins/trackmouse/trackmouse.cpp | 201 + .../src/plugins/trackmouse/trackmouse.h | 74 + .../src/plugins/trackmouse/trackmouse.kcfg | 21 + .../plugins/trackmouse/trackmouse_config.cpp | 103 + .../plugins/trackmouse/trackmouse_config.h | 40 + .../plugins/trackmouse/trackmouse_config.ui | 104 + .../plugins/trackmouse/trackmouseconfig.kcfgc | 5 + .../src/plugins/translucency/CMakeLists.txt | 1 + .../package/contents/code/main.js | 237 + .../package/contents/config/main.xml | 36 + .../package/contents/ui/config.ui | 473 ++ .../translucency/package/metadata.json | 155 + .../src/plugins/videowall/CMakeLists.txt | 1 + .../videowall/package/contents/code/main.js | 38 + .../package/contents/config/main.xml | 22 + .../videowall/package/contents/ui/config.ui | 148 + .../plugins/videowall/package/metadata.json | 152 + .../src/plugins/windowaperture/CMakeLists.txt | 1 + .../package/contents/code/main.js | 234 + .../windowaperture/package/metadata.json | 155 + .../src/plugins/windowsystem/CMakeLists.txt | 10 + .../plugins/windowsystem/kwindowsystem.json | 3 + .../src/plugins/windowsystem/plugin.cpp | 40 + .../source/src/plugins/windowsystem/plugin.h | 24 + .../plugins/windowsystem/windoweffects.cpp | 76 + .../src/plugins/windowsystem/windoweffects.h | 26 + .../src/plugins/windowsystem/windowshadow.cpp | 81 + .../src/plugins/windowsystem/windowshadow.h | 28 + .../src/plugins/windowsystem/windowsystem.cpp | 91 + .../src/plugins/windowsystem/windowsystem.h | 34 + .../src/plugins/windowview/CMakeLists.txt | 37 + .../src/plugins/windowview/kcm/CMakeLists.txt | 17 + .../windowview/kcm/windowvieweffectkcm.cpp | 92 + .../windowview/kcm/windowvieweffectkcm.h | 31 + .../windowview/kcm/windowvieweffectkcm.ui | 51 + .../source/src/plugins/windowview/main.cpp | 18 + .../src/plugins/windowview/metadata.json | 106 + .../org.kde.KWin.Effect.WindowView1.xml | 24 + .../plugins/windowview/windowviewconfig.kcfg | 27 + .../plugins/windowview/windowviewconfig.kcfgc | 10 + .../plugins/windowview/windowvieweffect.cpp | 415 ++ .../src/plugins/windowview/windowvieweffect.h | 121 + .../src/plugins/wobblywindows/CMakeLists.txt | 36 + .../source/src/plugins/wobblywindows/main.cpp | 18 + .../src/plugins/wobblywindows/metadata.json | 103 + .../plugins/wobblywindows/wobblywindows.cpp | 1171 ++++ .../src/plugins/wobblywindows/wobblywindows.h | 170 + .../plugins/wobblywindows/wobblywindows.kcfg | 57 + .../wobblywindows/wobblywindows_config.cpp | 103 + .../wobblywindows/wobblywindows_config.h | 37 + .../wobblywindows/wobblywindows_config.ui | 373 ++ .../wobblywindows/wobblywindowsconfig.kcfgc | 5 + .../source/src/plugins/zoom/CMakeLists.txt | 23 + .../kde/kwin/source/src/plugins/zoom/main.cpp | 17 + .../source/src/plugins/zoom/metadata.json | 97 + .../src/plugins/zoom/shaders/pixelgrid.frag | 25 + .../plugins/zoom/shaders/pixelgrid_core.frag | 24 + .../kde/kwin/source/src/plugins/zoom/zoom.cpp | 725 +++ .../kde/kwin/source/src/plugins/zoom/zoom.h | 156 + .../kwin/source/src/plugins/zoom/zoom.kcfg | 41 + .../kde/kwin/source/src/plugins/zoom/zoom.qrc | 6 + .../source/src/plugins/zoom/zoomconfig.kcfgc | 5 + .../kde/kwin/source/src/pointer_input.cpp | 1331 +++++ .../kde/kwin/source/src/pointer_input.h | 274 + .../kwin/source/src/popup_input_filter.cpp | 182 + .../kde/kwin/source/src/popup_input_filter.h | 36 + .../kde/kwin/source/src/qml/CMakeLists.txt | 3 + .../src/qml/frames/plasma/frame_none.qml | 39 + .../src/qml/frames/plasma/frame_styled.qml | 61 + .../src/qml/frames/plasma/frame_unstyled.qml | 53 + .../plasma/dummydata/osd.qml | 7 + .../qml/onscreennotification/plasma/main.qml | 35 + .../source/src/qml/outline/plasma/outline.qml | 127 + .../recipes/kde/kwin/source/src/resources.qrc | 13 + .../kde/kwin/source/src/rootinfo_filter.cpp | 29 + .../kde/kwin/source/src/rootinfo_filter.h | 33 + .../kde/kwin/source/src/rulebooksettings.cpp | 151 + .../kde/kwin/source/src/rulebooksettings.h | 49 + .../kwin/source/src/rulebooksettingsbase.kcfg | 17 + .../source/src/rulebooksettingsbase.kcfgc | 5 + local/recipes/kde/kwin/source/src/rules.cpp | 1083 ++++ local/recipes/kde/kwin/source/src/rules.h | 388 ++ .../kde/kwin/source/src/rulesettings.kcfg | 466 ++ .../kde/kwin/source/src/rulesettings.kcfgc | 7 + .../kde/kwin/source/src/scene/cursoritem.cpp | 79 + .../kde/kwin/source/src/scene/cursoritem.h | 37 + .../kwin/source/src/scene/decorationitem.cpp | 567 ++ .../kwin/source/src/scene/decorationitem.h | 163 + .../kde/kwin/source/src/scene/dndiconitem.cpp | 49 + .../kde/kwin/source/src/scene/dndiconitem.h | 38 + .../kde/kwin/source/src/scene/imageitem.cpp | 88 + .../kde/kwin/source/src/scene/imageitem.h | 51 + .../kde/kwin/source/src/scene/item.cpp | 738 +++ .../recipes/kde/kwin/source/src/scene/item.h | 242 + .../kwin/source/src/scene/itemgeometry.cpp | 300 ++ .../kde/kwin/source/src/scene/itemgeometry.h | 297 ++ .../kwin/source/src/scene/itemrenderer.cpp | 33 + .../kde/kwin/source/src/scene/itemrenderer.h | 45 + .../source/src/scene/itemrenderer_opengl.cpp | 547 ++ .../source/src/scene/itemrenderer_opengl.h | 92 + .../src/scene/itemrenderer_qpainter.cpp | 213 + .../source/src/scene/itemrenderer_qpainter.h | 44 + .../kde/kwin/source/src/scene/rootitem.cpp | 17 + .../kde/kwin/source/src/scene/rootitem.h | 25 + .../kde/kwin/source/src/scene/scene.cpp | 701 +++ .../recipes/kde/kwin/source/src/scene/scene.h | 260 + .../src/scene/shaders/debug_fractional.frag | 46 + .../src/scene/shaders/debug_fractional.vert | 43 + .../scene/shaders/debug_fractional_core.frag | 48 + .../scene/shaders/debug_fractional_core.vert | 45 + .../kde/kwin/source/src/scene/shadowitem.cpp | 512 ++ .../kde/kwin/source/src/scene/shadowitem.h | 86 + .../kde/kwin/source/src/scene/surfaceitem.cpp | 582 ++ .../kde/kwin/source/src/scene/surfaceitem.h | 205 + .../source/src/scene/surfaceitem_internal.cpp | 48 + .../source/src/scene/surfaceitem_internal.h | 38 + .../source/src/scene/surfaceitem_wayland.cpp | 316 ++ .../source/src/scene/surfaceitem_wayland.h | 95 + .../kde/kwin/source/src/scene/windowitem.cpp | 340 ++ .../kde/kwin/source/src/scene/windowitem.h | 138 + .../kwin/source/src/scene/workspacescene.cpp | 843 +++ .../kwin/source/src/scene/workspacescene.h | 122 + .../kde/kwin/source/src/screenedge.cpp | 1467 +++++ .../recipes/kde/kwin/source/src/screenedge.h | 604 +++ .../kwin/source/src/scripting/dbuscall.cpp | 50 + .../kde/kwin/source/src/scripting/dbuscall.h | 189 + .../src/scripting/desktopbackgrounditem.cpp | 126 + .../src/scripting/desktopbackgrounditem.h | 90 + .../scripting/documentation-effect-global.xml | 197 + .../src/scripting/documentation-global.xml | 144 + .../source/src/scripting/gesturehandler.cpp | 182 + .../source/src/scripting/gesturehandler.h | 265 + .../src/scripting/org.kde.kwin.Script.xml | 10 + .../src/scripting/screenedgehandler.cpp | 112 + .../source/src/scripting/screenedgehandler.h | 150 + .../source/src/scripting/scriptedeffect.cpp | 881 +++ .../source/src/scripting/scriptedeffect.h | 224 + .../scripting/scriptedquicksceneeffect.cpp | 135 + .../src/scripting/scriptedquicksceneeffect.h | 105 + .../kwin/source/src/scripting/scripting.cpp | 905 ++++ .../kde/kwin/source/src/scripting/scripting.h | 398 ++ .../src/scripting/scripting_logging.cpp | 10 + .../source/src/scripting/scripting_logging.h | 12 + .../source/src/scripting/scriptingutils.cpp | 76 + .../source/src/scripting/scriptingutils.h | 19 + .../source/src/scripting/shortcuthandler.cpp | 97 + .../source/src/scripting/shortcuthandler.h | 103 + .../kwin/source/src/scripting/tilemodel.cpp | 159 + .../kde/kwin/source/src/scripting/tilemodel.h | 67 + .../src/scripting/virtualdesktopmodel.cpp | 105 + .../src/scripting/virtualdesktopmodel.h | 47 + .../kwin/source/src/scripting/windowmodel.cpp | 332 ++ .../kwin/source/src/scripting/windowmodel.h | 124 + .../src/scripting/windowthumbnailitem.cpp | 417 ++ .../src/scripting/windowthumbnailitem.h | 110 + .../src/scripting/workspace_wrapper.cpp | 504 ++ .../source/src/scripting/workspace_wrapper.h | 463 ++ .../kde/kwin/source/src/settings.kcfgc | 6 + local/recipes/kde/kwin/source/src/shadow.cpp | 371 ++ local/recipes/kde/kwin/source/src/shadow.h | 146 + .../kde/kwin/source/src/shortcutdialog.ui | 104 + local/recipes/kde/kwin/source/src/sm.cpp | 479 ++ local/recipes/kde/kwin/source/src/sm.h | 127 + .../kwin/source/src/syncalarmx11filter.cpp | 36 + .../kde/kwin/source/src/syncalarmx11filter.h | 25 + .../kwin/source/src/tabbox/clientmodel.cpp | 272 + .../kde/kwin/source/src/tabbox/clientmodel.h | 98 + .../kwin/source/src/tabbox/switcheritem.cpp | 129 + .../kde/kwin/source/src/tabbox/switcheritem.h | 116 + .../src/tabbox/switchers/CMakeLists.txt | 6 + .../thumbnail_grid/contents/ui/main.qml | 233 + .../switchers/thumbnail_grid/metadata.json | 141 + .../kde/kwin/source/src/tabbox/tabbox.cpp | 1076 ++++ .../kde/kwin/source/src/tabbox/tabbox.h | 291 + .../kwin/source/src/tabbox/tabbox_logging.cpp | 10 + .../kwin/source/src/tabbox/tabbox_logging.h | 12 + .../kwin/source/src/tabbox/tabboxconfig.cpp | 187 + .../kde/kwin/source/src/tabbox/tabboxconfig.h | 290 + .../kwin/source/src/tabbox/tabboxhandler.cpp | 475 ++ .../kwin/source/src/tabbox/tabboxhandler.h | 280 + .../kde/kwin/source/src/tablet_input.cpp | 531 ++ .../kde/kwin/source/src/tablet_input.h | 83 + .../kde/kwin/source/src/tabletmodemanager.cpp | 217 + .../kde/kwin/source/src/tabletmodemanager.h | 60 + .../kde/kwin/source/src/tiles/customtile.cpp | 491 ++ .../kde/kwin/source/src/tiles/customtile.h | 82 + .../kde/kwin/source/src/tiles/quicktile.cpp | 256 + .../kde/kwin/source/src/tiles/quicktile.h | 57 + .../kde/kwin/source/src/tiles/tile.cpp | 592 ++ .../recipes/kde/kwin/source/src/tiles/tile.h | 184 + .../kde/kwin/source/src/tiles/tilemanager.cpp | 406 ++ .../kde/kwin/source/src/tiles/tilemanager.h | 87 + .../kde/kwin/source/src/touch_input.cpp | 210 + .../recipes/kde/kwin/source/src/touch_input.h | 77 + .../kde/kwin/source/src/useractions.cpp | 1756 ++++++ .../recipes/kde/kwin/source/src/useractions.h | 232 + .../kde/kwin/source/src/utils/CMakeLists.txt | 28 + .../recipes/kde/kwin/source/src/utils/c_ptr.h | 28 + .../kde/kwin/source/src/utils/common.cpp | 87 + .../kde/kwin/source/src/utils/common.h | 66 + .../kde/kwin/source/src/utils/cursortheme.cpp | 329 ++ .../kde/kwin/source/src/utils/cursortheme.h | 142 + .../kde/kwin/source/src/utils/damagejournal.h | 88 + .../source/src/utils/drm_format_helper.cpp | 173 + .../kwin/source/src/utils/drm_format_helper.h | 64 + .../kde/kwin/source/src/utils/edid.cpp | 419 ++ .../recipes/kde/kwin/source/src/utils/edid.h | 142 + .../kwin/source/src/utils/executable_path.h | 13 + .../source/src/utils/executable_path_proc.cpp | 14 + .../src/utils/executable_path_sysctl.cpp | 22 + .../kwin/source/src/utils/filedescriptor.cpp | 125 + .../kwin/source/src/utils/filedescriptor.h | 47 + .../kde/kwin/source/src/utils/kernel.h | 28 + .../recipes/kde/kwin/source/src/utils/keys.h | 59 + .../kde/kwin/source/src/utils/memorymap.h | 73 + .../source/src/utils/orientationsensor.cpp | 242 + .../kwin/source/src/utils/orientationsensor.h | 69 + .../recipes/kde/kwin/source/src/utils/pipe.h | 35 + .../kde/kwin/source/src/utils/ramfile.cpp | 164 + .../kde/kwin/source/src/utils/ramfile.h | 118 + .../kde/kwin/source/src/utils/realtime.cpp | 29 + .../kde/kwin/source/src/utils/realtime.h | 19 + .../kde/kwin/source/src/utils/resource.h | 27 + .../kde/kwin/source/src/utils/serviceutils.h | 74 + .../kde/kwin/source/src/utils/socketpair.h | 36 + .../source/src/utils/softwarevsyncmonitor.cpp | 60 + .../source/src/utils/softwarevsyncmonitor.h | 47 + .../source/src/utils/subsurfacemonitor.cpp | 85 + .../kwin/source/src/utils/subsurfacemonitor.h | 71 + .../kwin/source/src/utils/svgcursorreader.cpp | 147 + .../kwin/source/src/utils/svgcursorreader.h | 20 + .../kde/kwin/source/src/utils/udev.cpp | 295 + .../recipes/kde/kwin/source/src/utils/udev.h | 101 + .../kde/kwin/source/src/utils/version.cpp | 87 + .../kde/kwin/source/src/utils/version.h | 41 + .../kwin/source/src/utils/vsyncmonitor.cpp | 16 + .../kde/kwin/source/src/utils/vsyncmonitor.h | 36 + .../kde/kwin/source/src/utils/xcbutils.cpp | 650 +++ .../kde/kwin/source/src/utils/xcbutils.h | 2063 +++++++ .../kwin/source/src/utils/xcursorreader.cpp | 61 + .../kde/kwin/source/src/utils/xcursorreader.h | 20 + .../kde/kwin/source/src/virtualdesktops.cpp | 1014 ++++ .../kde/kwin/source/src/virtualdesktops.h | 590 ++ .../source/src/virtualdesktopsdbustypes.cpp | 58 + .../source/src/virtualdesktopsdbustypes.h | 34 + .../kwin/source/src/virtualkeyboard_dbus.cpp | 77 + .../kwin/source/src/virtualkeyboard_dbus.h | 52 + .../recipes/kde/kwin/source/src/watchdog.cpp | 76 + .../kwin/source/src/wayland/CMakeLists.txt | 294 + .../kde/kwin/source/src/wayland/DESIGN.md | 84 + .../src/wayland/abstract_data_source.cpp | 46 + .../source/src/wayland/abstract_data_source.h | 148 + .../src/wayland/abstract_drop_handler.cpp | 19 + .../src/wayland/abstract_drop_handler.h | 27 + .../source/src/wayland/alphamodifier_v1.cpp | 75 + .../source/src/wayland/alphamodifier_v1.h | 44 + .../kde/kwin/source/src/wayland/appmenu.cpp | 147 + .../kde/kwin/source/src/wayland/appmenu.h | 97 + .../kde/kwin/source/src/wayland/blur.cpp | 147 + .../kde/kwin/source/src/wayland/blur.h | 78 + .../source/src/wayland/clientconnection.cpp | 154 + .../source/src/wayland/clientconnection.h | 154 + .../source/src/wayland/colormanagement_v1.cpp | 718 +++ .../source/src/wayland/colormanagement_v1.h | 145 + .../kwin/source/src/wayland/compositor.cpp | 75 + .../kde/kwin/source/src/wayland/compositor.h | 48 + .../source/src/wayland/contenttype_v1.cpp | 86 + .../kwin/source/src/wayland/contenttype_v1.h | 44 + .../kde/kwin/source/src/wayland/contrast.cpp | 210 + .../kde/kwin/source/src/wayland/contrast.h | 80 + .../source/src/wayland/cursorshape_v1.cpp | 214 + .../kwin/source/src/wayland/cursorshape_v1.h | 31 + .../src/wayland/datacontroldevice_v1.cpp | 151 + .../source/src/wayland/datacontroldevice_v1.h | 59 + .../wayland/datacontroldevicemanager_v1.cpp | 80 + .../src/wayland/datacontroldevicemanager_v1.h | 42 + .../src/wayland/datacontroloffer_v1.cpp | 84 + .../source/src/wayland/datacontroloffer_v1.h | 47 + .../src/wayland/datacontrolsource_v1.cpp | 92 + .../source/src/wayland/datacontrolsource_v1.h | 48 + .../kwin/source/src/wayland/datadevice.cpp | 335 ++ .../kde/kwin/source/src/wayland/datadevice.h | 122 + .../kwin/source/src/wayland/datadevice_p.h | 54 + .../source/src/wayland/datadevicemanager.cpp | 78 + .../source/src/wayland/datadevicemanager.h | 42 + .../kde/kwin/source/src/wayland/dataoffer.cpp | 213 + .../kde/kwin/source/src/wayland/dataoffer.h | 74 + .../kwin/source/src/wayland/datasource.cpp | 197 + .../kde/kwin/source/src/wayland/datasource.h | 64 + .../kwin/source/src/wayland/datasource_p.h | 44 + .../kde/kwin/source/src/wayland/display.cpp | 301 ++ .../kde/kwin/source/src/wayland/display.h | 131 + .../kde/kwin/source/src/wayland/display_p.h | 71 + .../kde/kwin/source/src/wayland/dpms.cpp | 156 + .../kde/kwin/source/src/wayland/dpms.h | 63 + .../source/src/wayland/drmclientbuffer.cpp | 138 + .../kwin/source/src/wayland/drmclientbuffer.h | 44 + .../kwin/source/src/wayland/drmlease_v1.cpp | 456 ++ .../kde/kwin/source/src/wayland/drmlease_v1.h | 38 + .../kwin/source/src/wayland/drmlease_v1_p.h | 128 + .../src/wayland/externalbrightness_v1.cpp | 143 + .../src/wayland/externalbrightness_v1.h | 77 + .../source/src/wayland/filtered_display.cpp | 55 + .../source/src/wayland/filtered_display.h | 41 + .../kde/kwin/source/src/wayland/fixes.cpp | 50 + .../kde/kwin/source/src/wayland/fixes.h | 31 + .../source/src/wayland/fractionalscale_v1.cpp | 98 + .../source/src/wayland/fractionalscale_v1.h | 31 + .../source/src/wayland/fractionalscale_v1_p.h | 33 + .../src/wayland/frog_colormanagement_v1.cpp | 209 + .../src/wayland/frog_colormanagement_v1.h | 64 + .../kde/kwin/source/src/wayland/idle.cpp | 75 + .../kde/kwin/source/src/wayland/idle.h | 49 + .../kde/kwin/source/src/wayland/idle_p.h | 43 + .../source/src/wayland/idleinhibit_v1.cpp | 66 + .../kwin/source/src/wayland/idleinhibit_v1.h | 36 + .../source/src/wayland/idleinhibit_v1_p.h | 42 + .../kwin/source/src/wayland/idlenotify_v1.cpp | 98 + .../kwin/source/src/wayland/idlenotify_v1.h | 35 + .../source/src/wayland/inputmethod_v1.cpp | 528 ++ .../kwin/source/src/wayland/inputmethod_v1.h | 175 + .../kde/kwin/source/src/wayland/keyboard.cpp | 303 ++ .../kde/kwin/source/src/wayland/keyboard.h | 75 + .../kde/kwin/source/src/wayland/keyboard_p.h | 70 + .../wayland/keyboard_shortcuts_inhibit_v1.cpp | 192 + .../wayland/keyboard_shortcuts_inhibit_v1.h | 78 + .../kde/kwin/source/src/wayland/keystate.cpp | 88 + .../kde/kwin/source/src/wayland/keystate.h | 34 + .../kwin/source/src/wayland/layershell_v1.cpp | 533 ++ .../kwin/source/src/wayland/layershell_v1.h | 192 + .../src/wayland/linux_drm_syncobj_v1.cpp | 186 + .../source/src/wayland/linux_drm_syncobj_v1.h | 63 + .../src/wayland/linux_drm_syncobj_v1_p.h | 32 + .../src/wayland/linuxdmabufv1clientbuffer.cpp | 525 ++ .../src/wayland/linuxdmabufv1clientbuffer.h | 82 + .../src/wayland/linuxdmabufv1clientbuffer_p.h | 130 + .../src/wayland/lockscreen_overlay_v1.cpp | 56 + .../src/wayland/lockscreen_overlay_v1.h | 44 + .../kde/kwin/source/src/wayland/output.cpp | 331 ++ .../kde/kwin/source/src/wayland/output.h | 79 + .../source/src/wayland/output_order_v1.cpp | 74 + .../kwin/source/src/wayland/output_order_v1.h | 31 + .../source/src/wayland/outputdevice_v2.cpp | 1229 +++++ .../kwin/source/src/wayland/outputdevice_v2.h | 118 + .../src/wayland/outputmanagement_v2.cpp | 685 +++ .../source/src/wayland/outputmanagement_v2.h | 44 + .../kwin/source/src/wayland/plasmashell.cpp | 342 ++ .../kde/kwin/source/src/wayland/plasmashell.h | 238 + .../src/wayland/plasmavirtualdesktop.cpp | 354 ++ .../source/src/wayland/plasmavirtualdesktop.h | 159 + .../src/wayland/plasmawindowmanagement.cpp | 1193 +++++ .../src/wayland/plasmawindowmanagement.h | 317 ++ .../kde/kwin/source/src/wayland/pointer.cpp | 422 ++ .../kde/kwin/source/src/wayland/pointer.h | 107 + .../kde/kwin/source/src/wayland/pointer_p.h | 87 + .../src/wayland/pointerconstraints_v1.cpp | 338 ++ .../src/wayland/pointerconstraints_v1.h | 245 + .../src/wayland/pointerconstraints_v1_p.h | 101 + .../source/src/wayland/pointergestures_v1.cpp | 328 ++ .../source/src/wayland/pointergestures_v1.h | 38 + .../source/src/wayland/pointergestures_v1_p.h | 92 + .../source/src/wayland/presentationtime.cpp | 97 + .../source/src/wayland/presentationtime.h | 47 + .../src/wayland/primaryselectiondevice_v1.cpp | 118 + .../src/wayland/primaryselectiondevice_v1.h | 57 + .../primaryselectiondevicemanager_v1.cpp | 79 + .../primaryselectiondevicemanager_v1.h | 40 + .../src/wayland/primaryselectionoffer_v1.cpp | 86 + .../src/wayland/primaryselectionoffer_v1.h | 53 + .../src/wayland/primaryselectionsource_v1.cpp | 92 + .../src/wayland/primaryselectionsource_v1.h | 46 + .../source/src/wayland/protocols/README.md | 1 + .../protocols/frog-color-management-v1.xml | 356 ++ .../protocols/wlr-layer-shell-unstable-v1.xml | 342 ++ .../kde/kwin/source/src/wayland/quirks.h | 27 + .../kde/kwin/source/src/wayland/region.cpp | 47 + .../kde/kwin/source/src/wayland/region_p.h | 33 + .../source/src/wayland/relativepointer_v1.cpp | 98 + .../source/src/wayland/relativepointer_v1.h | 39 + .../source/src/wayland/relativepointer_v1_p.h | 45 + .../kwin/source/src/wayland/screencast_v1.cpp | 162 + .../kwin/source/src/wayland/screencast_v1.h | 73 + .../kwin/source/src/wayland/screenedge_v1.cpp | 150 + .../kwin/source/src/wayland/screenedge_v1.h | 59 + .../kde/kwin/source/src/wayland/seat.cpp | 1343 +++++ .../kde/kwin/source/src/wayland/seat.h | 702 +++ .../kde/kwin/source/src/wayland/seat_p.h | 162 + .../source/src/wayland/securitycontext_v1.cpp | 158 + .../source/src/wayland/securitycontext_v1.h | 25 + .../source/src/wayland/server_decoration.cpp | 236 + .../source/src/wayland/server_decoration.h | 132 + .../src/wayland/server_decoration_palette.cpp | 144 + .../src/wayland/server_decoration_palette.h | 85 + .../kde/kwin/source/src/wayland/shadow.cpp | 350 ++ .../kde/kwin/source/src/wayland/shadow.h | 60 + .../source/src/wayland/shmclientbuffer.cpp | 352 ++ .../kwin/source/src/wayland/shmclientbuffer.h | 35 + .../source/src/wayland/shmclientbuffer_p.h | 88 + .../kde/kwin/source/src/wayland/slide.cpp | 155 + .../kde/kwin/source/src/wayland/slide.h | 65 + .../kwin/source/src/wayland/subcompositor.cpp | 294 + .../kwin/source/src/wayland/subcompositor.h | 134 + .../kwin/source/src/wayland/subsurface_p.h | 54 + .../kde/kwin/source/src/wayland/surface.cpp | 1342 +++++ .../kde/kwin/source/src/wayland/surface.h | 587 ++ .../kde/kwin/source/src/wayland/surface_p.h | 237 + .../kde/kwin/source/src/wayland/tablet_v2.cpp | 1131 ++++ .../kde/kwin/source/src/wayland/tablet_v2.h | 325 ++ .../source/src/wayland/tearingcontrol_v1.cpp | 107 + .../source/src/wayland/tearingcontrol_v1.h | 31 + .../kde/kwin/source/src/wayland/textinput.cpp | 9 + .../kde/kwin/source/src/wayland/textinput.h | 148 + .../kwin/source/src/wayland/textinput_v1.cpp | 551 ++ .../kwin/source/src/wayland/textinput_v1.h | 296 + .../kwin/source/src/wayland/textinput_v1_p.h | 103 + .../kwin/source/src/wayland/textinput_v2.cpp | 560 ++ .../kwin/source/src/wayland/textinput_v2.h | 296 + .../kwin/source/src/wayland/textinput_v2_p.h | 86 + .../kwin/source/src/wayland/textinput_v3.cpp | 552 ++ .../kwin/source/src/wayland/textinput_v3.h | 211 + .../kwin/source/src/wayland/textinput_v3_p.h | 146 + .../source/src/wayland/tools/CMakeLists.txt | 94 + .../kwin/source/src/wayland/tools/README.md | 7 + .../src/wayland/tools/qtwaylandscanner.cpp | 1412 +++++ .../kde/kwin/source/src/wayland/touch.cpp | 130 + .../kde/kwin/source/src/wayland/touch.h | 48 + .../kde/kwin/source/src/wayland/touch_p.h | 35 + .../kwin/source/src/wayland/transaction.cpp | 289 + .../kde/kwin/source/src/wayland/transaction.h | 153 + .../kwin/source/src/wayland/viewporter.cpp | 148 + .../kde/kwin/source/src/wayland/viewporter.h | 39 + .../kwin/source/src/wayland/viewporter_p.h | 34 + .../source/src/wayland/xdgactivation_v1.cpp | 139 + .../source/src/wayland/xdgactivation_v1.h | 51 + .../source/src/wayland/xdgdecoration_v1.cpp | 144 + .../source/src/wayland/xdgdecoration_v1.h | 115 + .../source/src/wayland/xdgdecoration_v1_p.h | 43 + .../kwin/source/src/wayland/xdgdialog_v1.cpp | 152 + .../kwin/source/src/wayland/xdgdialog_v1.h | 58 + .../kwin/source/src/wayland/xdgforeign_v2.cpp | 295 + .../kwin/source/src/wayland/xdgforeign_v2.h | 84 + .../kwin/source/src/wayland/xdgforeign_v2_p.h | 122 + .../kwin/source/src/wayland/xdgoutput_v1.cpp | 219 + .../kwin/source/src/wayland/xdgoutput_v1.h | 37 + .../kde/kwin/source/src/wayland/xdgshell.cpp | 1305 +++++ .../kde/kwin/source/src/wayland/xdgshell.h | 597 +++ .../kde/kwin/source/src/wayland/xdgshell_p.h | 205 + .../source/src/wayland/xdgsystembell_v1.cpp | 67 + .../source/src/wayland/xdgsystembell_v1.h | 37 + .../source/src/wayland/xdgtopleveldrag_v1.cpp | 131 + .../source/src/wayland/xdgtopleveldrag_v1.h | 56 + .../source/src/wayland/xdgtoplevelicon_v1.cpp | 160 + .../source/src/wayland/xdgtoplevelicon_v1.h | 31 + .../src/wayland/xwaylandkeyboardgrab_v1.cpp | 112 + .../src/wayland/xwaylandkeyboardgrab_v1.h | 51 + .../source/src/wayland/xwaylandshell_v1.cpp | 177 + .../source/src/wayland/xwaylandshell_v1.h | 62 + .../kde/kwin/source/src/wayland_server.cpp | 908 ++++ .../kde/kwin/source/src/wayland_server.h | 315 ++ .../source/src/waylandshellintegration.cpp | 19 + .../kwin/source/src/waylandshellintegration.h | 25 + .../kde/kwin/source/src/waylandwindow.cpp | 267 + .../kde/kwin/source/src/waylandwindow.h | 55 + local/recipes/kde/kwin/source/src/window.cpp | 4748 +++++++++++++++++ local/recipes/kde/kwin/source/src/window.h | 2181 ++++++++ .../src/window_property_notify_x11_filter.cpp | 39 + .../src/window_property_notify_x11_filter.h | 28 + .../recipes/kde/kwin/source/src/workspace.cpp | 3156 +++++++++++ local/recipes/kde/kwin/source/src/workspace.h | 857 +++ .../kde/kwin/source/src/x11eventfilter.cpp | 51 + .../kde/kwin/source/src/x11eventfilter.h | 82 + .../recipes/kde/kwin/source/src/x11window.cpp | 4518 ++++++++++++++++ local/recipes/kde/kwin/source/src/x11window.h | 557 ++ .../kde/kwin/source/src/xdgactivationv1.cpp | 131 + .../kde/kwin/source/src/xdgactivationv1.h | 49 + .../kwin/source/src/xdgshellintegration.cpp | 87 + .../kde/kwin/source/src/xdgshellintegration.h | 38 + .../kde/kwin/source/src/xdgshellwindow.cpp | 2081 ++++++++ .../kde/kwin/source/src/xdgshellwindow.h | 326 ++ local/recipes/kde/kwin/source/src/xkb.cpp | 1388 +++++ local/recipes/kde/kwin/source/src/xkb.h | 221 + .../kwin/source/src/xwayland/CMakeLists.txt | 21 + .../kwin/source/src/xwayland/clipboard.cpp | 120 + .../kde/kwin/source/src/xwayland/clipboard.h | 40 + .../kwin/source/src/xwayland/databridge.cpp | 55 + .../kde/kwin/source/src/xwayland/databridge.h | 59 + .../kwin/source/src/xwayland/datasource.cpp | 84 + .../kde/kwin/source/src/xwayland/datasource.h | 79 + .../kde/kwin/source/src/xwayland/dnd.cpp | 200 + .../kde/kwin/source/src/xwayland/dnd.h | 67 + .../kde/kwin/source/src/xwayland/drag.cpp | 51 + .../kde/kwin/source/src/xwayland/drag.h | 47 + .../kde/kwin/source/src/xwayland/drag_wl.cpp | 355 ++ .../kde/kwin/source/src/xwayland/drag_wl.h | 122 + .../kde/kwin/source/src/xwayland/drag_x.cpp | 409 ++ .../kde/kwin/source/src/xwayland/drag_x.h | 135 + .../source/src/xwayland/lib/CMakeLists.txt | 18 + .../source/src/xwayland/lib/xauthority.cpp | 77 + .../kwin/source/src/xwayland/lib/xauthority.h | 14 + .../src/xwayland/lib/xwaylandsocket.cpp | 250 + .../source/src/xwayland/lib/xwaylandsocket.h | 40 + .../kde/kwin/source/src/xwayland/primary.cpp | 120 + .../kde/kwin/source/src/xwayland/primary.h | 41 + .../kwin/source/src/xwayland/selection.cpp | 446 ++ .../kde/kwin/source/src/xwayland/selection.h | 139 + .../kde/kwin/source/src/xwayland/transfer.cpp | 459 ++ .../kde/kwin/source/src/xwayland/transfer.h | 173 + .../kde/kwin/source/src/xwayland/xwayland.cpp | 628 +++ .../kde/kwin/source/src/xwayland/xwayland.h | 103 + .../source/src/xwayland/xwayland_interface.h | 36 + .../source/src/xwayland/xwaylandlauncher.cpp | 349 ++ .../source/src/xwayland/xwaylandlauncher.h | 121 + .../source/src/xwayland/xwldrophandler.cpp | 91 + .../kwin/source/src/xwayland/xwldrophandler.h | 43 + .../kde/kwin/source/tests/CMakeLists.txt | 98 + .../kde/kwin/source/tests/copyclient.cpp | 171 + .../kwin/source/tests/cursorhotspottest.cpp | 142 + .../kde/kwin/source/tests/dpmstest.cpp | 167 + .../kde/kwin/source/tests/fakeoutput.cpp | 104 + .../kde/kwin/source/tests/fakeoutput.h | 33 + .../kwin/source/tests/inputmethodstest.qml | 73 + .../source/tests/lockscreenoverlaytest.cpp | 90 + .../tests/lockscreenoverlaytest.desktop | 9 + .../source/tests/normalhintsbasesizetest.cpp | 107 + .../kde/kwin/source/tests/paneltest.cpp | 292 + .../kde/kwin/source/tests/pasteclient.cpp | 177 + .../kwin/source/tests/plasmasurfacetest.cpp | 200 + .../source/tests/pointerconstraintstest.cpp | 393 ++ .../source/tests/pointerconstraintstest.h | 179 + .../source/tests/pointerconstraintstest.qml | 205 + .../kwin/source/tests/pointergesturestest.cpp | 148 + .../kwin/source/tests/pointergesturestest.qml | 16 + .../kwin/source/tests/renderingservertest.cpp | 288 + .../kde/kwin/source/tests/shadowtest.cpp | 161 + .../kde/kwin/source/tests/subsurfacetest.cpp | 184 + .../kde/kwin/source/tests/touchclienttest.cpp | 205 + .../kde/kwin/source/tests/touchclienttest.h | 49 + .../kwin/source/tests/unmapdestroytest.qml | 72 + .../kwin/source/tests/waylandservertest.cpp | 113 + .../kde/kwin/source/tests/x11shadowreader.cpp | 117 + .../source/tests/xdgactivationtest-qt6.cpp | 27 + .../kde/kwin/source/tests/xdgforeigntest.cpp | 172 + .../recipes/kde/kwin/source/tests/xdgtest.cpp | 200 + local/recipes/kde/sddm/recipe.toml | 2 + local/recipes/kde/sddm/wayland-patch.sh | 7 + .../qtbase/source/src/corelib/CMakeLists.txt | 42 + .../qtbase/source/src/corelib/global/qtypes.h | 3 + .../socket/qnativesocketengine_unix.cpp | 6 + .../source/src/network/socket/qnet_unix_p.h | 3 + .../qwaylandclientbufferintegration_p.h | 12 + 2069 files changed, 362305 insertions(+), 6 deletions(-) create mode 100644 local/recipes/kde/kf6-ksvg/source/.git-blame-ignore-revs create mode 100644 local/recipes/kde/kf6-ksvg/source/.gitignore create mode 100644 local/recipes/kde/kf6-ksvg/source/.gitlab-ci.yml create mode 100644 local/recipes/kde/kf6-ksvg/source/.kde-ci.yml create mode 100644 local/recipes/kde/kf6-ksvg/source/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/KF6SvgConfig.cmake.in create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-2-Clause.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-3-Clause.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/CC0-1.0.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-only.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-or-later.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-3.0-only.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.0-or-later.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-only.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-or-later.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-3.0-only.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-Qt-Commercial.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/LICENSES/Qt-LGPL-exception-1.1.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/README.md create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/background.svgz create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/test_old_metadata_format_theme/metadata.desktop create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/colors create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/element.svg create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/metadata.json create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/opaque/element.svg create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/plasmarc create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.h create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/imagesettest.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/autotests/svgtest.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/metainfo.yaml create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ar/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ast/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/bg/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ca/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ca@valencia/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/cs/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/de/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/en_GB/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/eo/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/es/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/eu/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/fi/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/fr/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ga/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/gl/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/he/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/hi/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/hu/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ia/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/is/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/it/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ja/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ka/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ko/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/lt/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/nl/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/nn/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/pl/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/pt_BR/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ro/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/ru/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/sa/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/sk/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/sl/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/sv/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/tr/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/uk/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/po/zh_CN/libksvg6.po create mode 100644 local/recipes/kde/kf6-ksvg/source/src/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/src/Messages.sh create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdoc create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdocconf create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/declarativeimports/types.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/.krazy create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/README create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg-index.qdoc create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdoc create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdocconf create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_helpers.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_p.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/private/svg_p.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.h create mode 100644 local/recipes/kde/kf6-ksvg/source/src/tools/CMakeLists.txt create mode 100755 local/recipes/kde/kf6-ksvg/source/src/tools/apply-stylesheet.sh create mode 100755 local/recipes/kde/kf6-ksvg/source/src/tools/currentColorFillFix.sh create mode 100644 local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.inx create mode 100644 local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.py create mode 100644 local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/CMakeLists.txt create mode 100644 local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/split-plasma-svgs.cpp create mode 100644 local/recipes/kde/kf6-ksvg/source/tests/frames.qml create mode 100644 local/recipes/kde/kf6-ksvg/source/tests/selected_svg.qml create mode 100644 local/recipes/kde/kf6-ksvg/source/tests/shadows.qml create mode 100644 local/recipes/kde/kf6-ksvg/source/tests/testborders.qml create mode 100644 local/recipes/kde/kwin/source/.gitignore create mode 100644 local/recipes/kde/kwin/source/.gitlab-ci.yml create mode 100644 local/recipes/kde/kwin/source/.kde-ci.yml create mode 100644 local/recipes/kde/kwin/source/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/CONTRIBUTING.md create mode 100644 local/recipes/kde/kwin/source/KWinDBusInterfaceConfig.cmake.in create mode 100644 local/recipes/kde/kwin/source/LICENSES/BSD-3-Clause.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/CC0-1.0.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/GPL-2.0-only.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/GPL-2.0-or-later.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/GPL-3.0-only.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/GPL-3.0-or-later.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-only.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-or-later.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-only.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-or-later.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LGPL-3.0-only.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt create mode 100644 local/recipes/kde/kwin/source/LICENSES/MIT.txt create mode 100644 local/recipes/kde/kwin/source/Mainpage.dox create mode 100644 local/recipes/kde/kwin/source/README.md create mode 100644 local/recipes/kde/kwin/source/autotests/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/data/Framework 13.icc create mode 100644 local/recipes/kde/kwin/source/autotests/data/Samsung CRG49 Shaper Matrix.icc create mode 100644 local/recipes/kde/kwin/source/autotests/drm/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/drm/mock_drm.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/drm/mock_drm.h create mode 100644 local/recipes/kde/kwin/source/autotests/effect/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/V3D-V3D_4_2-desktop-2.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/VC4-V3D_2_1-desktop-2.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-bonaire-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-cayman-gles-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-hawaii-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-navi-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-r9-290-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-5700-xt-4.6 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-redwood-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-tonga-4.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-broadwell-gt2-3.3 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-haswell-mobile-3.3 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.3 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-mobile-3.3 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-kabylake-gt2-4.6 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-sandybridge-mobile-3.3 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-skylake-gt2-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/lima-mali400-desktop-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-10.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-5.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-560-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-660-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-950-4.5 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970M-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-980-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/panfrost-malit860-desktop-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/data/glplatform/virgl-3.1 create mode 100644 local/recipes/kde/kwin/source/autotests/effect/kwinglplatformtest.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/effect/timelinetest.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/effect/windowquadlisttest.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/activation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/activities_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/bounce_keys_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/buttonrebind_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/data/anim-data-delete-effect/effect.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/data/example.desktop create mode 100644 local/recipes/kde/kwin/source/autotests/integration/data/rules/force-maximize create mode 100644 local/recipes/kde/kwin/source/autotests/integration/data/rules/maximize-vert-apply-initial create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dbus_interface_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/debug_console_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/decoration_input_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_aurorae_destroy_deco.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_cancel_animation.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_empty_deco.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_glxgears.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_reinitialize_compositor.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/dont_crash_useractions_menu.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/desktop_switching_animation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/maximize_animation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/minimize_animation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/popup_open_close_animation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripted_effects_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTestMulti.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/completeTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectContext.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectsHandler.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTestGlobal.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTestMulti.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/grabAlreadyGrabbedWindowForcedTest_owner.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/grabAlreadyGrabbedWindowForcedTest_thief.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/grabAlreadyGrabbedWindowTest_grabber.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/grabAlreadyGrabbedWindowTest_owner.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/grabTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/keepAliveTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/keepAliveTestDontKeep.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectAnimateDontTerminateTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetTerminateTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTouchTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/shortcutsTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/scripts/ungrabTest.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/slidingpopups_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/toplevel_open_close_animation_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/effects/translucency_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fakeinput_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fakes/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.json create mode 100644 local/recipes/kde/kwin/source/autotests/integration/fractional_scaling_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.h create mode 100644 local/recipes/kde/kwin/source/autotests/integration/globalshortcuts_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/helper/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/helper/kill.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/idle_inhibition_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/input_capture_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/input_stacking_order.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/inputmethod_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/internal_window.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/keyboard_input_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/keyboard_layout_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/keymap_creation_failure_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.h create mode 100644 local/recipes/kde/kwin/source/autotests/integration/kwinbindings_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/layershellv1window_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/lockscreen.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/maximize_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/mouseactions_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/move_resize_window_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/no_global_shortcuts_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/outputchanges_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/placement_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/plasma_surface_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/plasmawindow_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/platformcursor.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/pointer_constraints_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/pointer_input.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/quick_tiling_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scene_opengl_es_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scene_opengl_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/screen_changes_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/screencasting_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/screenedges_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/screens_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/minimizeall_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/screenedge_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedge.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgetouch.qml create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgeunregister.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/touchScreenedge.js create mode 100644 local/recipes/kde/kwin/source/autotests/integration/security_context_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/showing_desktop_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/stacking_order_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/sticky_keys_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/tabbox_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/test_colormanagement.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/test_helpers.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/test_virtualkeyboard_dbus.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/tiles_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/touch_input_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/transient_placement.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/virtual_desktop_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/window_rules_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/window_selection_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/workspace_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/x11_window_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/x11keyread.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_rules_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xinerama_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xwayland_input_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_crash_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_restart_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/device_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/gesture_event_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/key_event_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.h create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/pointer_event_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/libinput/touch_event_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/onscreennotificationtest.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/onscreennotificationtest.h create mode 100644 local/recipes/kde/kwin/source/autotests/opengl_context_attribute_builder_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/output_transform_test.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_client_machine.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_colorspaces.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_ftrace.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_gestures.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_utils.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_virtual_desktops.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_window_paint_data.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_xcb_size_hints.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_xcb_window.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_xcb_wrapper.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/test_xkb.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/testutils.h create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_datasource.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_error.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_activities.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_virtual_desktop.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_plasmashell.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_pointer_constraints.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration_palette.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_shadow.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_shm_pool.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_text_input_v2.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_appmenu.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_blur.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_contrast.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_filter.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_output.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_seat.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_slide.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_subsurface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_surface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_windowmanagement.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_decoration.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_foreign.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_output.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_shell.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_datacontrol_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_display.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_inputmethod_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_keyboard_shortcuts_inhibitor_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_layershellv1_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_no_xdg_runtime_dir.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_screencast.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_seat.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_tablet_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv1_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv3_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/wayland/server/test_viewporter_interface.cpp create mode 100644 local/recipes/kde/kwin/source/autotests/xcb_scaling_mock.cpp create mode 100644 local/recipes/kde/kwin/source/cmake/modules/FindLibdrm.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/FindLibeis-1.0.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/FindXKB.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/FindXwayland.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/Findgbm.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/Findhwdata.cmake create mode 100644 local/recipes/kde/kwin/source/cmake/modules/Findlcms2.cmake create mode 100644 local/recipes/kde/kwin/source/data/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/data/icons/16-apps-kwin.png create mode 100644 local/recipes/kde/kwin/source/data/icons/32-apps-kwin.png create mode 100644 local/recipes/kde/kwin/source/data/icons/48-apps-kwin.png create mode 100644 local/recipes/kde/kwin/source/data/icons/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/data/icons/sc-apps-kwin.svgz create mode 100644 local/recipes/kde/kwin/source/data/org_kde_kwin.categories create mode 100644 local/recipes/kde/kwin/source/data/update_default_rules.cpp create mode 100644 local/recipes/kde/kwin/source/doc/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/TESTING.md create mode 100644 local/recipes/kde/kwin/source/doc/coding-conventions.md create mode 100644 local/recipes/kde/kwin/source/doc/desktop/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/desktop/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/button.png create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/configure.png create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/decoration.png create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwindecoration/main.png create mode 100644 local/recipes/kde/kwin/source/doc/kwineffects/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwineffects/configure-effects.png create mode 100644 local/recipes/kde/kwin/source/doc/kwineffects/dialog-information.png create mode 100644 local/recipes/kde/kwin/source/doc/kwineffects/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwineffects/video.png create mode 100644 local/recipes/kde/kwin/source/doc/kwinscreenedges/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwinscreenedges/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwintabbox/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwintabbox/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwintabbox/taskswitcher.png create mode 100644 local/recipes/kde/kwin/source/doc/kwintouchscreen/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwintouchscreen/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.pdf create mode 100755 local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.tex create mode 100644 local/recipes/kde/kwin/source/doc/windowbehaviour/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/windowbehaviour/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/Face-smile.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/akgregator-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/akregator-attributes.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/akregator-fav.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/config-win-behavior.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/emacs-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/emacs-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/focus-stealing-pop2top-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/index.docbook create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/knotes-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/knotes-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kopete-attribute-2.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kopete-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-detect-window.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-kopete-rules.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-rule-editor.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main-n-akregator.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-ordering.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-attributes.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-matching.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/pager-4-desktops.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-attribute.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-attribute-2.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-info.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-emacs.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-init.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-knotes.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete-chat.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-ready-akregator.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-compose.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-main.png create mode 100644 local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-reminder.png create mode 100644 local/recipes/kde/kwin/source/examples/plugin/.gitignore create mode 100644 local/recipes/kde/kwin/source/examples/plugin/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/examples/plugin/eventlistener.cpp create mode 100644 local/recipes/kde/kwin/source/examples/plugin/eventlistener.h create mode 100644 local/recipes/kde/kwin/source/examples/plugin/main.cpp create mode 100644 local/recipes/kde/kwin/source/examples/plugin/metadata.json create mode 100644 local/recipes/kde/kwin/source/examples/plugin/metadata.json.license create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/.gitignore create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml.license create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui.license create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json.license create mode 100644 local/recipes/kde/kwin/source/examples/quick-script/.gitignore create mode 100644 local/recipes/kde/kwin/source/examples/quick-script/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/examples/quick-script/package/contents/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json.license create mode 100644 local/recipes/kde/kwin/source/kconf_update/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/kconf_update/kwin-6.0-delete-desktop-switching-shortcuts.cpp create mode 100644 local/recipes/kde/kwin/source/kconf_update/kwin-6.0-remove-breeze-tabbox-default.cpp create mode 100644 local/recipes/kde/kwin/source/kconf_update/kwin-6.0-reset-active-mouse-screen.cpp create mode 100644 local/recipes/kde/kwin/source/kconf_update/kwin-6.1-remove-gridview-expose-shortcuts.cpp create mode 100644 local/recipes/kde/kwin/source/kconf_update/kwin.upd create mode 100644 local/recipes/kde/kwin/source/logo.png create mode 100644 local/recipes/kde/kwin/source/plasma-kwin_wayland.service.in create mode 100644 local/recipes/kde/kwin/source/src/3rdparty/colortemperature.h create mode 100644 local/recipes/kde/kwin/source/src/3rdparty/xcursor.c create mode 100644 local/recipes/kde/kwin/source/src/3rdparty/xcursor.h create mode 100644 local/recipes/kde/kwin/source/src/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/activation.cpp create mode 100644 local/recipes/kde/kwin/source/src/activities.cpp create mode 100644 local/recipes/kde/kwin/source/src/activities.h create mode 100644 local/recipes/kde/kwin/source/src/appmenu.cpp create mode 100644 local/recipes/kde/kwin/source/src/appmenu.h create mode 100644 local/recipes/kde/kwin/source/src/atoms.cpp create mode 100644 local/recipes/kde/kwin/source/src/atoms.h create mode 100644 local/recipes/kde/kwin/source/src/backends/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_blob.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_blob.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_commit.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_commit.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_connector.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_connector.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_layer.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_layer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_logging.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_object.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_object.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_output.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_output.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline_legacy.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_plane.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_plane.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_pointer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_property.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_property.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_render_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.h create mode 100644 local/recipes/kde/kwin/source/src/backends/drm/overview.md create mode 100644 local/recipes/kde/kwin/source/src/backends/fakeinput/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/connection.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/connection.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/context.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/context.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/device.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/device.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/events.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/events.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.h create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.h create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.h create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.h create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.cpp create mode 100644 local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.h create mode 100644 local/recipes/kde/kwin/source/src/backends/x11/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/client_machine.cpp create mode 100644 local/recipes/kde/kwin/source/src/client_machine.h create mode 100644 local/recipes/kde/kwin/source/src/compositor.cpp create mode 100644 local/recipes/kde/kwin/source/src/compositor.h create mode 100644 local/recipes/kde/kwin/source/src/config-kwin.h.cmake create mode 100644 local/recipes/kde/kwin/source/src/core/brightnessdevice.h create mode 100644 local/recipes/kde/kwin/source/src/core/colorlut3d.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/colorlut3d.h create mode 100644 local/recipes/kde/kwin/source/src/core/colorpipeline.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/colorpipeline.h create mode 100644 local/recipes/kde/kwin/source/src/core/colorpipelinestage.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/colorpipelinestage.h create mode 100644 local/recipes/kde/kwin/source/src/core/colorspace.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/colorspace.h create mode 100644 local/recipes/kde/kwin/source/src/core/colortransformation.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/colortransformation.h create mode 100644 local/recipes/kde/kwin/source/src/core/drmdevice.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/drmdevice.h create mode 100644 local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.h create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.h create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbufferview.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/graphicsbufferview.h create mode 100644 local/recipes/kde/kwin/source/src/core/iccprofile.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/iccprofile.h create mode 100644 local/recipes/kde/kwin/source/src/core/inputbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/inputbackend.h create mode 100644 local/recipes/kde/kwin/source/src/core/inputdevice.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/inputdevice.h create mode 100644 local/recipes/kde/kwin/source/src/core/output.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/output.h create mode 100644 local/recipes/kde/kwin/source/src/core/outputbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/outputbackend.h create mode 100644 local/recipes/kde/kwin/source/src/core/outputconfiguration.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/outputconfiguration.h create mode 100644 local/recipes/kde/kwin/source/src/core/outputlayer.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/outputlayer.h create mode 100644 local/recipes/kde/kwin/source/src/core/pixelgrid.h create mode 100644 local/recipes/kde/kwin/source/src/core/renderbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/renderbackend.h create mode 100644 local/recipes/kde/kwin/source/src/core/renderjournal.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/renderjournal.h create mode 100644 local/recipes/kde/kwin/source/src/core/renderloop.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/renderloop.h create mode 100644 local/recipes/kde/kwin/source/src/core/renderloop_p.h create mode 100644 local/recipes/kde/kwin/source/src/core/rendertarget.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/rendertarget.h create mode 100644 local/recipes/kde/kwin/source/src/core/renderviewport.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/renderviewport.h create mode 100644 local/recipes/kde/kwin/source/src/core/session.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/session.h create mode 100644 local/recipes/kde/kwin/source/src/core/session_consolekit.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/session_consolekit.h create mode 100644 local/recipes/kde/kwin/source/src/core/session_logind.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/session_logind.h create mode 100644 local/recipes/kde/kwin/source/src/core/session_noop.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/session_noop.h create mode 100644 local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.h create mode 100644 local/recipes/kde/kwin/source/src/core/syncobjtimeline.cpp create mode 100644 local/recipes/kde/kwin/source/src/core/syncobjtimeline.h create mode 100644 local/recipes/kde/kwin/source/src/cursor.cpp create mode 100644 local/recipes/kde/kwin/source/src/cursor.h create mode 100644 local/recipes/kde/kwin/source/src/cursorsource.cpp create mode 100644 local/recipes/kde/kwin/source/src/cursorsource.h create mode 100644 local/recipes/kde/kwin/source/src/dbusinterface.cpp create mode 100644 local/recipes/kde/kwin/source/src/dbusinterface.h create mode 100644 local/recipes/kde/kwin/source/src/debug_console.cpp create mode 100644 local/recipes/kde/kwin/source/src/debug_console.h create mode 100644 local/recipes/kde/kwin/source/src/debug_console.ui create mode 100644 local/recipes/kde/kwin/source/src/decorations/decoratedwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/decorations/decoratedwindow.h create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorationbridge.cpp create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorationbridge.h create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorationpalette.cpp create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorationpalette.h create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorations_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/decorations/decorations_logging.h create mode 100644 local/recipes/kde/kwin/source/src/decorations/settings.cpp create mode 100644 local/recipes/kde/kwin/source/src/decorations/settings.h create mode 100644 local/recipes/kde/kwin/source/src/dpmsinputeventfilter.cpp create mode 100644 local/recipes/kde/kwin/source/src/dpmsinputeventfilter.h create mode 100644 local/recipes/kde/kwin/source/src/effect/anidata.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/anidata_p.h create mode 100644 local/recipes/kde/kwin/source/src/effect/animationeffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/animationeffect.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effect.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effect.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effectframe.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effectframe.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effecthandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effecthandler.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effectloader.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effectloader.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effecttogglablestate.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effecttogglablestate.h create mode 100644 local/recipes/kde/kwin/source/src/effect/effectwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/effectwindow.h create mode 100644 local/recipes/kde/kwin/source/src/effect/globals.h create mode 100644 local/recipes/kde/kwin/source/src/effect/logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/logging_p.h create mode 100644 local/recipes/kde/kwin/source/src/effect/offscreeneffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/offscreeneffect.h create mode 100644 local/recipes/kde/kwin/source/src/effect/offscreenquickview.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/offscreenquickview.h create mode 100644 local/recipes/kde/kwin/source/src/effect/quickeffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/quickeffect.h create mode 100644 local/recipes/kde/kwin/source/src/effect/timeline.cpp create mode 100644 local/recipes/kde/kwin/source/src/effect/timeline.h create mode 100644 local/recipes/kde/kwin/source/src/effect/xcb.h create mode 100644 local/recipes/kde/kwin/source/src/events.cpp create mode 100644 local/recipes/kde/kwin/source/src/focuschain.cpp create mode 100644 local/recipes/kde/kwin/source/src/focuschain.h create mode 100644 local/recipes/kde/kwin/source/src/ftrace.cpp create mode 100644 local/recipes/kde/kwin/source/src/ftrace.h create mode 100644 local/recipes/kde/kwin/source/src/gestures.cpp create mode 100644 local/recipes/kde/kwin/source/src/gestures.h create mode 100644 local/recipes/kde/kwin/source/src/globalshortcuts.cpp create mode 100644 local/recipes/kde/kwin/source/src/globalshortcuts.h create mode 100644 local/recipes/kde/kwin/source/src/group.cpp create mode 100644 local/recipes/kde/kwin/source/src/group.h create mode 100644 local/recipes/kde/kwin/source/src/helpers/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/helpers/killer/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/helpers/killer/killer.cpp create mode 100755 local/recipes/kde/kwin/source/src/helpers/killer/org.kde.kwin.killer.desktop.in create mode 100644 local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/kwin_wrapper.cpp create mode 100644 local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.c create mode 100644 local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.h create mode 100644 local/recipes/kde/kwin/source/src/hide_cursor_spy.cpp create mode 100644 local/recipes/kde/kwin/source/src/hide_cursor_spy.h create mode 100644 local/recipes/kde/kwin/source/src/idle_inhibition.cpp create mode 100644 local/recipes/kde/kwin/source/src/idle_inhibition.h create mode 100644 local/recipes/kde/kwin/source/src/idledetector.cpp create mode 100644 local/recipes/kde/kwin/source/src/idledetector.h create mode 100644 local/recipes/kde/kwin/source/src/input.cpp create mode 100644 local/recipes/kde/kwin/source/src/input.h create mode 100644 local/recipes/kde/kwin/source/src/input_event.cpp create mode 100644 local/recipes/kde/kwin/source/src/input_event.h create mode 100644 local/recipes/kde/kwin/source/src/input_event_spy.cpp create mode 100644 local/recipes/kde/kwin/source/src/input_event_spy.h create mode 100644 local/recipes/kde/kwin/source/src/inputmethod.cpp create mode 100644 local/recipes/kde/kwin/source/src/inputmethod.h create mode 100644 local/recipes/kde/kwin/source/src/inputpanelv1integration.cpp create mode 100644 local/recipes/kde/kwin/source/src/inputpanelv1integration.h create mode 100644 local/recipes/kde/kwin/source/src/inputpanelv1window.cpp create mode 100644 local/recipes/kde/kwin/source/src/inputpanelv1window.h create mode 100644 local/recipes/kde/kwin/source/src/internalwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/internalwindow.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/types.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kcm.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kcm.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kcm_kwindecoration.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kwin-applywindowdecoration.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/ui/ButtonGroup.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/ui/Buttons.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/ui/ConfigureTitlebar.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/ui/Themes.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/utils.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/utils.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/decoration/window-decorations.knsrc.cmake create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/kcm_kwin_virtualdesktops.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/kcm.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/kcm.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/kcm_kwin_effects.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/kwineffect.knsrc create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/ui/Effect.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/effects/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/AUTHORS create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/ChangeLog create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/actions.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/advanced.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/focus.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/kcm_kwinoptions.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/main.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/mouse.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/mouse.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/mouse.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/moving.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/windows.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/options/windows.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/kcm_kwinrules.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/FileDialogLoader.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/OptionsComboBox.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/RuleItemDelegate.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/RulesEditor.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/ValueEditor.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/rules/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwinscreenedges.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwintouchscreen.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/main.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/main.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/touch.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/touch.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/screenedges/touch.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/CMakeLists.txt create mode 100755 local/recipes/kde/kwin/source/src/kcms/scripts/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/kcm_kwin_scripts.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/kwinscripts.knsrc create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/module.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/module.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/scripts/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kcm_kwintabbox.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcher.knsrc create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/main.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/main.ui create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/desktop.png create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/dolphin.png create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/falkon.png create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/kmail.png create mode 100644 local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/systemsettings.png create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcm_virtualkeyboard.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/Messages.sh create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/kcm_kwinxwayland.json create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.cpp create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.h create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/kcms/xwayland/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/keyboard_input.cpp create mode 100644 local/recipes/kde/kwin/source/src/keyboard_input.h create mode 100644 local/recipes/kde/kwin/source/src/keyboard_layout.cpp create mode 100644 local/recipes/kde/kwin/source/src/keyboard_layout.h create mode 100644 local/recipes/kde/kwin/source/src/keyboard_layout_switching.cpp create mode 100644 local/recipes/kde/kwin/source/src/keyboard_layout_switching.h create mode 100644 local/recipes/kde/kwin/source/src/keyboard_repeat.cpp create mode 100644 local/recipes/kde/kwin/source/src/keyboard_repeat.h create mode 100644 local/recipes/kde/kwin/source/src/killprompt.cpp create mode 100644 local/recipes/kde/kwin/source/src/killprompt.h create mode 100644 local/recipes/kde/kwin/source/src/killwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/killwindow.h create mode 100644 local/recipes/kde/kwin/source/src/kscreenintegration.cpp create mode 100644 local/recipes/kde/kwin/source/src/kscreenintegration.h create mode 100644 local/recipes/kde/kwin/source/src/kwin.kcfg create mode 100644 local/recipes/kde/kwin/source/src/kwin.notifyrc create mode 100644 local/recipes/kde/kwin/source/src/layers.cpp create mode 100644 local/recipes/kde/kwin/source/src/layershellv1integration.cpp create mode 100644 local/recipes/kde/kwin/source/src/layershellv1integration.h create mode 100644 local/recipes/kde/kwin/source/src/layershellv1window.cpp create mode 100644 local/recipes/kde/kwin/source/src/layershellv1window.h create mode 100644 local/recipes/kde/kwin/source/src/lidswitchtracker.cpp create mode 100644 local/recipes/kde/kwin/source/src/lidswitchtracker.h create mode 100644 local/recipes/kde/kwin/source/src/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/main.h create mode 100644 local/recipes/kde/kwin/source/src/main_wayland.cpp create mode 100644 local/recipes/kde/kwin/source/src/main_wayland.h create mode 100644 local/recipes/kde/kwin/source/src/mousebuttons.cpp create mode 100644 local/recipes/kde/kwin/source/src/mousebuttons.h create mode 100644 local/recipes/kde/kwin/source/src/netinfo.cpp create mode 100644 local/recipes/kde/kwin/source/src/netinfo.h create mode 100644 local/recipes/kde/kwin/source/src/onscreennotification.cpp create mode 100644 local/recipes/kde/kwin/source/src/onscreennotification.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/colormanagement.glsl create mode 100644 local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglcontext.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglcontext.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/egldisplay.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/egldisplay.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglimagetexture.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglimagetexture.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglnativefence.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglnativefence.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglswapchain.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglswapchain.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/eglutils_p.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glframebuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glframebuffer.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/gllut.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/gllut.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/gllut3D.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/gllut3D.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glplatform.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glplatform.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glrendertimequery.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glrendertimequery.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glshader.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glshader.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glshadermanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glshadermanager.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/gltexture.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/gltexture.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/gltexture_p.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glutils.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glutils.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/glvertexbuffer_p.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/icc.frag create mode 100644 local/recipes/kde/kwin/source/src/opengl/icc_core.frag create mode 100644 local/recipes/kde/kwin/source/src/opengl/icc_shader.cpp create mode 100644 local/recipes/kde/kwin/source/src/opengl/icc_shader.h create mode 100644 local/recipes/kde/kwin/source/src/opengl/saturation.glsl create mode 100644 local/recipes/kde/kwin/source/src/options.cpp create mode 100644 local/recipes/kde/kwin/source/src/options.h create mode 100644 local/recipes/kde/kwin/source/src/org.freedesktop.DBus.Properties.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.KWin.Plugins.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.KWin.Session.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.KWin.VirtualDesktopManager.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.KWin.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.kappmenu.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.kwin.Compositing.xml create mode 100644 local/recipes/kde/kwin/source/src/org.kde.kwin.Effects.xml create mode 100644 local/recipes/kde/kwin/source/src/osd.cpp create mode 100644 local/recipes/kde/kwin/source/src/osd.h create mode 100644 local/recipes/kde/kwin/source/src/outline.cpp create mode 100644 local/recipes/kde/kwin/source/src/outline.h create mode 100644 local/recipes/kde/kwin/source/src/outputconfigurationstore.cpp create mode 100644 local/recipes/kde/kwin/source/src/outputconfigurationstore.h create mode 100644 local/recipes/kde/kwin/source/src/placeholderinputeventfilter.cpp create mode 100644 local/recipes/kde/kwin/source/src/placeholderinputeventfilter.h create mode 100644 local/recipes/kde/kwin/source/src/placeholderoutput.cpp create mode 100644 local/recipes/kde/kwin/source/src/placeholderoutput.h create mode 100644 local/recipes/kde/kwin/source/src/placement.cpp create mode 100644 local/recipes/kde/kwin/source/src/placement.h create mode 100644 local/recipes/kde/kwin/source/src/placementtracker.cpp create mode 100644 local/recipes/kde/kwin/source/src/placementtracker.h create mode 100644 local/recipes/kde/kwin/source/src/plugin.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugin.h create mode 100644 local/recipes/kde/kwin/source/src/pluginmanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/pluginmanager.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/blendchanges/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/blendchanges/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/blendchanges/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blur_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/blurconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex.vert create mode 100644 local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex_core.vert create mode 100644 local/recipes/kde/kwin/source/src/plugins/bouncekeys/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/bouncekeys/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/bouncekeys/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/buttonrebinds/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/buttonrebinds/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/buttonrebinds/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json.license create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/README create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorpicker/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorpicker/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/colorpicker/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/osd.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/dialogparent/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/dialogparent/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/dialogparent/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/diminactiveconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/diminactive/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/dimscreen/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/dimscreen/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/dimscreen/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/eis/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/eyeonscreen/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/fade/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/fade/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/fade/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/fade/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadedesktop/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadingpopups/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/fallapartconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/fallapart/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/frozenapp/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/frozenapp/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/frozenapp/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/fullscreen/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/fullscreen/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/fullscreen/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glide_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/glideconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/glide/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/hidecursor/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/highlightwindow/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/highlightwindow/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/highlightwindow/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/idletime/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/idletime/kwin.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/idletime/poller.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/idletime/poller.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/invert.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/invert.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/invert.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/keynotification/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/keynotification/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/keynotification/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kglobalaccel/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kwin.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/dbusutils_p.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/kwin-runner-windows.desktop create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/org.kde.krunner1.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/kscreenconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/kscreen/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/login/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/login/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/login/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/login/package/contents/ui/config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/login/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/logout/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/logout/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/logout/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclampconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/magiclamp/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/magnifierconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/magnifier/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/maximize/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/maximize/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/maximize/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/minimizeall/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/minimizeall/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/minimizeall/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclickconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/mousemark/mousemarkconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/constants.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/nightlight/org.kde.KWin.NightLight.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/outputlocator/qml/OutputLabel.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/kcm/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopBar.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopView.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/expoarea.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/expoarea.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/expolayout.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/expolayout.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/plugin.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/plugin.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeap.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeapDelegate.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/integration.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/integration.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/kwin.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/screen.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/screen.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/window.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/qpa/window.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/scale/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/scale/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/scale/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/scale/package/contents/ui/config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/scale/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/screencastutils.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenedge/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenedge/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenedge/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/org.kde.KWin.ScreenShot2.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.vert create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.vert create mode 100644 local/recipes/kde/kwin/source/src/plugins/sessionquit/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/sessionquit/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/sessionquit/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/sheet.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/sheet.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/sheet.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/sheet/sheetconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/showcompositing/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/showcompositing/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showcompositing/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/showfps/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/showfps/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showfps/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/showpaint/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/showpaint/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showpaint/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slide_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/slideconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/springmotion.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slide/springmotion.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/slideback.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slideback/slideback.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopupsconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/squash/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/squash/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/squash/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/stickykeys/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/stickykeys/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/stickykeys/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.h create mode 100755 local/recipes/kde/kwin/source/src/plugins/strip-effect-metadata.py create mode 100644 local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/systembell.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/systembell.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/systembell/systembell.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailasideconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeCorner.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeHandle.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/TileDelegate.qml create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/layouts.svg create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/touchpoints/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/touchpoints/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/touchpoints/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_inner.png create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_outer.png create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouseconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/translucency/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/ui/config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/translucency/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/videowall/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/config/main.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/ui/config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/videowall/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowaperture/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowaperture/package/contents/code/main.js create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowaperture/package/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/kwindowsystem.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/kcm/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/org.kde.KWin.Effect.WindowView1.xml create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.ui create mode 100644 local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindowsconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/main.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid_core.frag create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/zoom.cpp create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/zoom.h create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/zoom.kcfg create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/zoom.qrc create mode 100644 local/recipes/kde/kwin/source/src/plugins/zoom/zoomconfig.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/pointer_input.cpp create mode 100644 local/recipes/kde/kwin/source/src/pointer_input.h create mode 100644 local/recipes/kde/kwin/source/src/popup_input_filter.cpp create mode 100644 local/recipes/kde/kwin/source/src/popup_input_filter.h create mode 100644 local/recipes/kde/kwin/source/src/qml/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_none.qml create mode 100644 local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_styled.qml create mode 100644 local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_unstyled.qml create mode 100644 local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/dummydata/osd.qml create mode 100644 local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/main.qml create mode 100644 local/recipes/kde/kwin/source/src/qml/outline/plasma/outline.qml create mode 100644 local/recipes/kde/kwin/source/src/resources.qrc create mode 100644 local/recipes/kde/kwin/source/src/rootinfo_filter.cpp create mode 100644 local/recipes/kde/kwin/source/src/rootinfo_filter.h create mode 100644 local/recipes/kde/kwin/source/src/rulebooksettings.cpp create mode 100644 local/recipes/kde/kwin/source/src/rulebooksettings.h create mode 100644 local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfg create mode 100644 local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/rules.cpp create mode 100644 local/recipes/kde/kwin/source/src/rules.h create mode 100644 local/recipes/kde/kwin/source/src/rulesettings.kcfg create mode 100644 local/recipes/kde/kwin/source/src/rulesettings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/scene/cursoritem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/cursoritem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/decorationitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/decorationitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/dndiconitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/dndiconitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/imageitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/imageitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/item.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/item.h create mode 100644 local/recipes/kde/kwin/source/src/scene/itemgeometry.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/itemgeometry.h create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer.h create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.h create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.h create mode 100644 local/recipes/kde/kwin/source/src/scene/rootitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/rootitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/scene.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/scene.h create mode 100644 local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.frag create mode 100644 local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.vert create mode 100644 local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.frag create mode 100644 local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.vert create mode 100644 local/recipes/kde/kwin/source/src/scene/shadowitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/shadowitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.h create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.h create mode 100644 local/recipes/kde/kwin/source/src/scene/windowitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/windowitem.h create mode 100644 local/recipes/kde/kwin/source/src/scene/workspacescene.cpp create mode 100644 local/recipes/kde/kwin/source/src/scene/workspacescene.h create mode 100644 local/recipes/kde/kwin/source/src/screenedge.cpp create mode 100644 local/recipes/kde/kwin/source/src/screenedge.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/dbuscall.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/dbuscall.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/documentation-effect-global.xml create mode 100644 local/recipes/kde/kwin/source/src/scripting/documentation-global.xml create mode 100644 local/recipes/kde/kwin/source/src/scripting/gesturehandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/gesturehandler.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/org.kde.kwin.Script.xml create mode 100644 local/recipes/kde/kwin/source/src/scripting/screenedgehandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/screenedgehandler.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptedeffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptedeffect.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/scripting.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/scripting.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/scripting_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/scripting_logging.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptingutils.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/scriptingutils.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/shortcuthandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/shortcuthandler.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/tilemodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/tilemodel.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/virtualdesktopmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/virtualdesktopmodel.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/windowmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/windowmodel.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/windowthumbnailitem.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/windowthumbnailitem.h create mode 100644 local/recipes/kde/kwin/source/src/scripting/workspace_wrapper.cpp create mode 100644 local/recipes/kde/kwin/source/src/scripting/workspace_wrapper.h create mode 100644 local/recipes/kde/kwin/source/src/settings.kcfgc create mode 100644 local/recipes/kde/kwin/source/src/shadow.cpp create mode 100644 local/recipes/kde/kwin/source/src/shadow.h create mode 100644 local/recipes/kde/kwin/source/src/shortcutdialog.ui create mode 100644 local/recipes/kde/kwin/source/src/sm.cpp create mode 100644 local/recipes/kde/kwin/source/src/sm.h create mode 100644 local/recipes/kde/kwin/source/src/syncalarmx11filter.cpp create mode 100644 local/recipes/kde/kwin/source/src/syncalarmx11filter.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/clientmodel.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/clientmodel.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/switcheritem.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/switcheritem.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/switchers/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml create mode 100644 local/recipes/kde/kwin/source/src/tabbox/switchers/thumbnail_grid/metadata.json create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabbox.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabbox.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabbox_logging.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabbox_logging.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabboxconfig.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabboxconfig.h create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabboxhandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabbox/tabboxhandler.h create mode 100644 local/recipes/kde/kwin/source/src/tablet_input.cpp create mode 100644 local/recipes/kde/kwin/source/src/tablet_input.h create mode 100644 local/recipes/kde/kwin/source/src/tabletmodemanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/tabletmodemanager.h create mode 100644 local/recipes/kde/kwin/source/src/tiles/customtile.cpp create mode 100644 local/recipes/kde/kwin/source/src/tiles/customtile.h create mode 100644 local/recipes/kde/kwin/source/src/tiles/quicktile.cpp create mode 100644 local/recipes/kde/kwin/source/src/tiles/quicktile.h create mode 100644 local/recipes/kde/kwin/source/src/tiles/tile.cpp create mode 100644 local/recipes/kde/kwin/source/src/tiles/tile.h create mode 100644 local/recipes/kde/kwin/source/src/tiles/tilemanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/tiles/tilemanager.h create mode 100644 local/recipes/kde/kwin/source/src/touch_input.cpp create mode 100644 local/recipes/kde/kwin/source/src/touch_input.h create mode 100644 local/recipes/kde/kwin/source/src/useractions.cpp create mode 100644 local/recipes/kde/kwin/source/src/useractions.h create mode 100644 local/recipes/kde/kwin/source/src/utils/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/utils/c_ptr.h create mode 100644 local/recipes/kde/kwin/source/src/utils/common.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/common.h create mode 100644 local/recipes/kde/kwin/source/src/utils/cursortheme.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/cursortheme.h create mode 100644 local/recipes/kde/kwin/source/src/utils/damagejournal.h create mode 100644 local/recipes/kde/kwin/source/src/utils/drm_format_helper.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/drm_format_helper.h create mode 100644 local/recipes/kde/kwin/source/src/utils/edid.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/edid.h create mode 100644 local/recipes/kde/kwin/source/src/utils/executable_path.h create mode 100644 local/recipes/kde/kwin/source/src/utils/executable_path_proc.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/executable_path_sysctl.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/filedescriptor.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/filedescriptor.h create mode 100644 local/recipes/kde/kwin/source/src/utils/kernel.h create mode 100644 local/recipes/kde/kwin/source/src/utils/keys.h create mode 100644 local/recipes/kde/kwin/source/src/utils/memorymap.h create mode 100644 local/recipes/kde/kwin/source/src/utils/orientationsensor.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/orientationsensor.h create mode 100644 local/recipes/kde/kwin/source/src/utils/pipe.h create mode 100644 local/recipes/kde/kwin/source/src/utils/ramfile.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/ramfile.h create mode 100644 local/recipes/kde/kwin/source/src/utils/realtime.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/realtime.h create mode 100644 local/recipes/kde/kwin/source/src/utils/resource.h create mode 100644 local/recipes/kde/kwin/source/src/utils/serviceutils.h create mode 100644 local/recipes/kde/kwin/source/src/utils/socketpair.h create mode 100644 local/recipes/kde/kwin/source/src/utils/softwarevsyncmonitor.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/softwarevsyncmonitor.h create mode 100644 local/recipes/kde/kwin/source/src/utils/subsurfacemonitor.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/subsurfacemonitor.h create mode 100644 local/recipes/kde/kwin/source/src/utils/svgcursorreader.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/svgcursorreader.h create mode 100644 local/recipes/kde/kwin/source/src/utils/udev.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/udev.h create mode 100644 local/recipes/kde/kwin/source/src/utils/version.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/version.h create mode 100644 local/recipes/kde/kwin/source/src/utils/vsyncmonitor.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/vsyncmonitor.h create mode 100644 local/recipes/kde/kwin/source/src/utils/xcbutils.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/xcbutils.h create mode 100644 local/recipes/kde/kwin/source/src/utils/xcursorreader.cpp create mode 100644 local/recipes/kde/kwin/source/src/utils/xcursorreader.h create mode 100644 local/recipes/kde/kwin/source/src/virtualdesktops.cpp create mode 100644 local/recipes/kde/kwin/source/src/virtualdesktops.h create mode 100644 local/recipes/kde/kwin/source/src/virtualdesktopsdbustypes.cpp create mode 100644 local/recipes/kde/kwin/source/src/virtualdesktopsdbustypes.h create mode 100644 local/recipes/kde/kwin/source/src/virtualkeyboard_dbus.cpp create mode 100644 local/recipes/kde/kwin/source/src/virtualkeyboard_dbus.h create mode 100644 local/recipes/kde/kwin/source/src/watchdog.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/wayland/DESIGN.md create mode 100644 local/recipes/kde/kwin/source/src/wayland/abstract_data_source.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/abstract_data_source.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/abstract_drop_handler.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/abstract_drop_handler.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/alphamodifier_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/alphamodifier_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/appmenu.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/appmenu.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/blur.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/blur.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/clientconnection.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/clientconnection.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/colormanagement_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/colormanagement_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/compositor.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/compositor.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/contenttype_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/contenttype_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/contrast.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/contrast.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/cursorshape_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/cursorshape_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroldevice_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroldevice_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroldevicemanager_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroldevicemanager_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroloffer_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontroloffer_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontrolsource_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datacontrolsource_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datadevice.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datadevice.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datadevice_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datadevicemanager.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datadevicemanager.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/dataoffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/dataoffer.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datasource.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/datasource.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/datasource_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/display.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/display.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/display_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/dpms.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/dpms.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/drmclientbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/drmclientbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/drmlease_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/drmlease_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/drmlease_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/externalbrightness_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/externalbrightness_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/filtered_display.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/filtered_display.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/fixes.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/fixes.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/fractionalscale_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/fractionalscale_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/fractionalscale_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/frog_colormanagement_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/frog_colormanagement_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/idle.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/idle.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/idle_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/idleinhibit_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/idleinhibit_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/idleinhibit_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/idlenotify_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/idlenotify_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/inputmethod_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/inputmethod_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/keyboard.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/keyboard.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/keyboard_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/keyboard_shortcuts_inhibit_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/keyboard_shortcuts_inhibit_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/keystate.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/keystate.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/layershell_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/layershell_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/linux_drm_syncobj_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/linux_drm_syncobj_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/linux_drm_syncobj_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/linuxdmabufv1clientbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/linuxdmabufv1clientbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/linuxdmabufv1clientbuffer_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/lockscreen_overlay_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/lockscreen_overlay_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/output.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/output.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/output_order_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/output_order_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/outputdevice_v2.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/outputdevice_v2.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/outputmanagement_v2.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/outputmanagement_v2.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmashell.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmashell.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmavirtualdesktop.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmavirtualdesktop.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmawindowmanagement.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/plasmawindowmanagement.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointer.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointer.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointer_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointerconstraints_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointerconstraints_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointerconstraints_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointergestures_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointergestures_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/pointergestures_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/presentationtime.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/presentationtime.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectiondevice_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectiondevice_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectiondevicemanager_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectiondevicemanager_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectionoffer_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectionoffer_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectionsource_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/primaryselectionsource_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/protocols/README.md create mode 100644 local/recipes/kde/kwin/source/src/wayland/protocols/frog-color-management-v1.xml create mode 100644 local/recipes/kde/kwin/source/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml create mode 100644 local/recipes/kde/kwin/source/src/wayland/quirks.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/region.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/region_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/relativepointer_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/relativepointer_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/relativepointer_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/screencast_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/screencast_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/screenedge_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/screenedge_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/seat.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/seat.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/seat_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/securitycontext_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/securitycontext_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/server_decoration.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/server_decoration.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/server_decoration_palette.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/server_decoration_palette.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/shadow.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/shadow.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/shmclientbuffer.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/shmclientbuffer.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/shmclientbuffer_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/slide.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/slide.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/subcompositor.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/subcompositor.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/subsurface_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/surface.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/surface.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/surface_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/tablet_v2.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/tablet_v2.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/tearingcontrol_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/tearingcontrol_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v2.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v2.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v2_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v3.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v3.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/textinput_v3_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/tools/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/wayland/tools/README.md create mode 100644 local/recipes/kde/kwin/source/src/wayland/tools/qtwaylandscanner.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/touch.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/touch.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/touch_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/transaction.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/transaction.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/viewporter.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/viewporter.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/viewporter_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgactivation_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgactivation_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgdecoration_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgdecoration_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgdecoration_v1_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgdialog_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgdialog_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgforeign_v2.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgforeign_v2.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgforeign_v2_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgoutput_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgoutput_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgshell.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgshell.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgshell_p.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgsystembell_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgsystembell_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgtopleveldrag_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgtopleveldrag_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgtoplevelicon_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xdgtoplevelicon_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xwaylandkeyboardgrab_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xwaylandkeyboardgrab_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland/xwaylandshell_v1.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland/xwaylandshell_v1.h create mode 100644 local/recipes/kde/kwin/source/src/wayland_server.cpp create mode 100644 local/recipes/kde/kwin/source/src/wayland_server.h create mode 100644 local/recipes/kde/kwin/source/src/waylandshellintegration.cpp create mode 100644 local/recipes/kde/kwin/source/src/waylandshellintegration.h create mode 100644 local/recipes/kde/kwin/source/src/waylandwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/waylandwindow.h create mode 100644 local/recipes/kde/kwin/source/src/window.cpp create mode 100644 local/recipes/kde/kwin/source/src/window.h create mode 100644 local/recipes/kde/kwin/source/src/window_property_notify_x11_filter.cpp create mode 100644 local/recipes/kde/kwin/source/src/window_property_notify_x11_filter.h create mode 100644 local/recipes/kde/kwin/source/src/workspace.cpp create mode 100644 local/recipes/kde/kwin/source/src/workspace.h create mode 100644 local/recipes/kde/kwin/source/src/x11eventfilter.cpp create mode 100644 local/recipes/kde/kwin/source/src/x11eventfilter.h create mode 100644 local/recipes/kde/kwin/source/src/x11window.cpp create mode 100644 local/recipes/kde/kwin/source/src/x11window.h create mode 100644 local/recipes/kde/kwin/source/src/xdgactivationv1.cpp create mode 100644 local/recipes/kde/kwin/source/src/xdgactivationv1.h create mode 100644 local/recipes/kde/kwin/source/src/xdgshellintegration.cpp create mode 100644 local/recipes/kde/kwin/source/src/xdgshellintegration.h create mode 100644 local/recipes/kde/kwin/source/src/xdgshellwindow.cpp create mode 100644 local/recipes/kde/kwin/source/src/xdgshellwindow.h create mode 100644 local/recipes/kde/kwin/source/src/xkb.cpp create mode 100644 local/recipes/kde/kwin/source/src/xkb.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/xwayland/clipboard.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/clipboard.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/databridge.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/databridge.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/datasource.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/datasource.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/dnd.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/dnd.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag_wl.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag_wl.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag_x.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/drag_x.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/lib/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/src/xwayland/lib/xauthority.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/lib/xauthority.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/lib/xwaylandsocket.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/lib/xwaylandsocket.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/primary.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/primary.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/selection.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/selection.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/transfer.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/transfer.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwayland.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwayland.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwayland_interface.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwaylandlauncher.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwaylandlauncher.h create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwldrophandler.cpp create mode 100644 local/recipes/kde/kwin/source/src/xwayland/xwldrophandler.h create mode 100644 local/recipes/kde/kwin/source/tests/CMakeLists.txt create mode 100644 local/recipes/kde/kwin/source/tests/copyclient.cpp create mode 100644 local/recipes/kde/kwin/source/tests/cursorhotspottest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/dpmstest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/fakeoutput.cpp create mode 100644 local/recipes/kde/kwin/source/tests/fakeoutput.h create mode 100644 local/recipes/kde/kwin/source/tests/inputmethodstest.qml create mode 100644 local/recipes/kde/kwin/source/tests/lockscreenoverlaytest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/lockscreenoverlaytest.desktop create mode 100644 local/recipes/kde/kwin/source/tests/normalhintsbasesizetest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/paneltest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/pasteclient.cpp create mode 100644 local/recipes/kde/kwin/source/tests/plasmasurfacetest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/pointerconstraintstest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/pointerconstraintstest.h create mode 100644 local/recipes/kde/kwin/source/tests/pointerconstraintstest.qml create mode 100644 local/recipes/kde/kwin/source/tests/pointergesturestest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/pointergesturestest.qml create mode 100644 local/recipes/kde/kwin/source/tests/renderingservertest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/shadowtest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/subsurfacetest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/touchclienttest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/touchclienttest.h create mode 100644 local/recipes/kde/kwin/source/tests/unmapdestroytest.qml create mode 100644 local/recipes/kde/kwin/source/tests/waylandservertest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/x11shadowreader.cpp create mode 100644 local/recipes/kde/kwin/source/tests/xdgactivationtest-qt6.cpp create mode 100644 local/recipes/kde/kwin/source/tests/xdgforeigntest.cpp create mode 100644 local/recipes/kde/kwin/source/tests/xdgtest.cpp 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 f0ae5e6c274ecbe5bdc13e183c4b0da275d6d8b8..464cf0d0601978f73b7ee5138a0c85a1f72a39d3 100644 GIT binary patch literal 85400 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 literal 86436 zcmV(mK=Z%-H+ooF000E$*0e?hz~Q0=fRH930UQ7T0002fOrmAx@tg1eT>v$l%O3G3}`P4(Jjg|fzK7tD8^w&cGUS*UL6Mt-HSC63N|G2 z(+LP93wi!65&o=qE_%SZVP={^<$3+62XlR&-Sb}{B2FTl(JWpPII?EVbO86Z*2_V0 zKEjZkF2_YSVwb-r&PHawNMOz_g3?^h;{|d+cuZxnIgBmMfLe1^oca)_GKkkWDaQZs zgwqZ&l$4g=#Dk+(vp3fiO+02ahhaNE7WEj6?t%H`#k)ZW7&RjBfUMj#HjD+E9}LUb z!Y^>e3P(bFZkW*Ty;fYLz*<`?cb*$`Et^hw+w}P+x*NF;3tQKRR*06*We437Hy+Fp zl1%nI*C5>q?ldcB#Wk25w6PkID(e7kg`_P`24~p`zHnH0T-!Cejk|0y$r7uOJqDJa zuUk!)lKHBPLyVKX94AMscO2gBnD~~h@9;kM!+40*={;H|V9xfwuwMCW^kqVkQ7%qX z?|Zlic$iur^isy)C=<;|oThBs1&I|kaILVdb|PEzXS?Kr%}U3h5{w=f5i`Ts{x`o4 z-X8#UX-pput&ykr(!<9=q8)71gEl z<>z4(9(qT?sE!5JFwAn?)RkbaW?VsO$6muc%-H;7pe{UsJOwM{gSd+DnK?y~(f$w) zsPU$A{+r|HS-oW#TM;*_Kxc@-2R%m}#xtDI1N2?&GD519*- ztIA5gZTwKal0Jx%`S2jkob=WSmtAp!eu^{?8ytLCNw1jKN)=5}kDLO`O3!nJiQZ1y zsNml_E!IyDEr{U2&*}QaID@<#Ey&VT=popk#d=uLKMopx^L(d-n7!Ug!7`{ytvga< zi6LN};^D)jamG5>1)h6ogOSYr$SX@K~?$E(G;{_ zF|3bsiUAXZXouJ0y zItrl2B63ym0!_^_{Jfsy+~$~K#bMSE(AN65ph1a>@m)6ZqEf$Ezty^;OV*Ad50VkCz-wM~%FXLugoW)IrUrONS?F%qE?8#=!-lSPG*~5oQQoJSBaL8K4Pc z5292D7n|n?($ev`E}*Kj`WNE%rW5}TWW~fCG49BB8TTFVNp0{(SOT&B!5#GgWrvQ^ zLb{g8S{-BW!atW^&QHp-n{RUV#t=s>Wz%wZO>kGI#&|u26_*lYU~d4)qx~Xe*W*_a z-ccS_lE;yc;3nib^QZ9OFc>J#rVCt}(wC^QbVv-rf}JuRksX{$4bLZ2qoRQZIbGAa zZIT=4?12Kd%WTK?bl3K@t5k(Om$Wz$N*Uiku-qJJie0n+Zx*u-R5WVEx1YgK8e%2 zU2`0nh1HIn3L{9fbPAYfdlqZb&e3|ms@@YsKpc&$HbnqY;{!tiZ;$M!VRK-uaO|le z^)UZoE16ZV$icf$mcg#jjd(Adl^Kxz`v0PQZ24oeg>_F>U1{Tk(N81LIVO#fBZ5iN z8v+GbeT~*}VViG_gePwgGBt(`hC$i1X7Z0p%e9v!Y%w4M4U7Sko@TY>uFG}&@&ZyG zmp6;lcaApuH~G12GJ%w4h?X2(~!=b&7fGnW0w?B5gv^Dv6IC_g;t^Z*4vF44sB#x zuc-azOiSFUs8(t0Nvz2$o4B?PpV0r-1?E}=$P8;=+WQ8|p=u;xqzxZOen;8)Ic#l&kRyOWwN4Pi4d0}wC zMTG|CQXSgG>h^!5&Q4TJ+)vJ1{4Yo9a_8m$Mr=J7SeAUG3@JNkL1PSmQH7*yBLpNE zKt0s09YBJaRFU}tC)gWHw~nGnt?X5~B$G+RtfA{#E?7ms!`rn2A{5375N^}xOhlji z?wV5;+I0{zFM;=^%t+GNZj;QHfnKgrr7Pk0t+!U&&4gJV1o=196oe zV8O%Q(QIvqN+RQTD~Rj9AFk^SEfS*OlHdnd+tw#cG)_bzUVPx`7fhG+bM<3z3oT^fm6;4d+DzFaYZ%gq@3G@;qjCQZl5{}=e zl>GU^82q53+c_9aq%U&lJ!SLPuidd;ulQ!VJemi)3K`QPl(0rU&WijM{3Ez|nB4`o zL1BTuZ0eP;o!guvoe5UK^32Kn&_LN_w6g@hEC8=5N2iJnqE|}FXCs-XXg;rS0ZF3g z^4h&kPcau0Q6Frb-p&0>`QsHwTI|zRk8FCK3!Z=5&o~ypoZspRN;+P74&ExBlVxn_ zLDPqXvLFf6Ae=G%9dtQl$Sz2mT54Vtm8+Pn$b&Oz0n)TKPr8&f?Mq|$B<(L-*yjTO z4dlqt6n5Z&*U(La!^_P?kF08P2RRasaw{rPc%P~Njy)@;w{l#?(ZwouACZfrKecCN zEraDRE=G2!qNUrw+6Xe6gZXP7c%kUXO=gZMl+mot&Wx;(-^N^qye;9ZpQk&^q+oxN zaWm8#?A}_Ig?_Rb9Y`8^q!F)Lt_Gd!Ec~Yrm?PheL@d~WvvCVBhCdo`w8uvq{-Qyk z%=kqOKo*6y7KKydm1cj05DH>6XtYdA@p0CA)Fr-qUzy-_RY6*GQsm4nw2~1(k6`{F z32rJ7xY){_AmQEeFGVdVj);EY>fe>E!)PWnTcB-Ea;RU_x{jEW45?{MC@B0ePU6BD zW59wmwncxWeqA~a zfgk(Qd>Wv5O4NzY)qUd3z`qFBcAsxy9CuQ*d_=d_B?xL#I_>Ehh33)ifqoz+QYNhZ z3FSLsw=!s@FN=d0OWz*dDFtR}r8V^agCtu&i%exTzQY&;t9`0p*;{i-Ah~e}2)Ds@ zD@=iTF_}l8iA7#@I5ycDk&RaH#V1*ND+aoEBWUXp1?TEV= z*m!D*+9(VXLnQXZLdszhxbv#tvOunlgC;vl%vZW#Cd0!r^m{UUhbkTt(^Utn0UZMH zK>)()N3c9S7^3$okPIBmYpI#SS|kI?uy%<;vJ-^>(pB@uFc7mY{MwI!K8Gt@!du8n z(J=|iIh+ctECEFdCZzFM%Z;yrhHi}&H`jayXN;Jf{V3+xA@pOZn=N(K_u;Xseu?7& zd6|%!(B9~)Sj${{{$h?f4iLc=`nwB+HwJ2$aXa)3D%xwQu?bPxR!B3CaX+h4Z&nZ2 zCJgUnlt@)Ahxt~`NMe{RJJS!8Wa<2*M+xOkB8P}fFb9x-u)5s?AF9W888WF$m>_Iu8 z|H*EY9~?ie)G2O)>j@$7DiyDHtqwv?G8nx{z;v__pbTTHEM ztMR1Y!voE9I~Yix6%572)x@S5)PIT}{2?Ej!h|8bdgr|FG5_ba=QA9leB$BpWw4Li zPaZ2@eb@l^x;u-=?HY0GHi?yBJfdO%8gto{?9cv%^sxh~|9mApX30B(MNcZ2MGv5w zbV9m%ko#Qb6Mqtk*b*t|7op!`tSxbTd$ua6o{=0FJC9Ca^z}ul7xA82i82V+VUAM# zJf2wI@HmvtD5pgs@@a_SPnTUZaiYG#r?C&D2L!gsuPBlt(4l@W2VD-TO5t&r@Pfu1 z)RCp1uMr0zINDX%lozLL1q4s|dOj_f`XuBwCF*<1>=D|hmnt_=bGn_PspazaXV z$p6cga=?}T-N+j62SJvofD2~Go1^=r0xH!{4jT8c^$r>_X{?KfV2I~L^k8TN@fVx> zp|cxNJtKiFdmc_3Euq%-Ff`yM#4P)S_whgQFk~LUAltBeB3gvz zF*l8qsE8QGI?$aVa@E%%395Zn9_s~G9*eM>q&JC8m+E5s7r<~w;ANd?K4V5rbQ-U@ zj5@FM5QZx@&P7_5YMV>pqT=wKygbQyVo>ik_Q6qTbM$NiIs9Uiq=|!FuyZv6mhL7; zd8*<8!(og6--XZW@KikzS3?!VYY0lKykT+f|nlwl)g zpP}z31#_#PpEXgQ5FcHma~l07hjS;km?dQaG8R@4xYtNO!EMLu?NnvEE^#F9;tElNnX^! zg%oce+L4f$F~`eqahtf6t-qP3PVmfb?WU7^@H7Gf4;pw1hhO*WIpBHAx3_9}x?jhe z3)^usI+!y#n0C3u`0wPjGwLZz!mg zX*pkGhX-tL?m%3}Er)-c#BuG_k#)7*5@wL?&4TG`XH3l@8#o9W=y3YZ?TV zNv-Aog?Kz(pbPBl4m#e@uHX;xKH2YA`~!AnMmlI$QO}7^=<+b|c**l+Dvg+$ewHhCN zq5ZtAk6K&Sn$;fSnmJ~2UD`u~Vmr-5+JtP0-BZdUNNDZ+sN2$MQkg`Y!i)O)F*d4e zJ1@o097Djb>92kFlgh4BH%=C~N8kpeHHd0Hjp&goTnUu7b|GKyFu$)IM*f$2rtnT?dFMBY#L zviUE^eml{SYZ_WVb3YX!Syc6*sicH0;%6#u%husBcx5`^c=}*N+tytq#91x73k$KM!_(M7u7EM9KSiNb)_s{W2&}a^+*yB$0yeV z)|o5#OK{%_wK0;E6JTTXu0N#(Arw7xm^11tF7rf&4q`2%u$4epKj>Lf{-u*mW8XGs z3m<+%xpbhY+;o(1U~WIyAf*6|CnXRKtu<1`o|{ zl~1}_#wHe}FxWB|D2`;9PvQuzE2X*t3?HjS#HRlJmlse|+4SSzH8FRah~*YS$#Q4s z%DX?SAj}t=+fEySW=O1!avQ2}4VItB+h$d_)+S*|eAvi_*35wXl($b9a&%q0$kq`F zm{z=kTLB^F_!+C2BQ2Aow1(gDp7*=kFHN)RLPa(WiP0Cu3d7s~^VtTd2_f;OXk(o{ z&T+&TO5j9~i$R9)4XM{;GSE<8#|zmd&|Jt!TRtms9_5mpqhIwMD^BHm9>uf7gg>L$ z8G6PE4h>5|_hg+Qi`Ra{@n{~&imT$LRs~=y9?i;09w;gnXn!&*Im>DZji(wbhtLZS zGH2Atp#!|dm8%C?$~;T6lc3>@Vfgljk03D=E8SWRb-)irzityyd$0NLu0KRqvTd$^ z7Q2W{>KxF`=lp4N_TI}S*?*K!MRx?fM`%G#N=(o13oU$ciJQu{#$a5^hMhS?X- z5q+li#u^5t1gtOT=u^~*3&+#x($kS@<*2v0TDB+2PvW$%3h_u_k~1F}fuh`oc!(Df zaR#w~W}bTwBksj&_`PFsi^wcwaqC^>+hWU=BeQnQ38E6#NFW!GzwGYQ zaXEk=r%v7;yaEHWYt>x;&GX9H1n!&G#?k3-*T0eE0}S$EKB4h>ufu_BMPWW+a_v9o z!S+HR9wa)Bkc(-0XN|LF^G%os81`AFvDZ=dJluZJsL1or<7TEU8Yk%Oor(1R^@+wS z6;UV-25hiVuDlR4M-;t!xldgh2~8FhZnZpVmZ1<0r0ZU$_Oj)cXOL-+j?gs z1422-9I2D@`5<~56|04NuvO2hLj;|gZgvBHt_v-mjOPJ@$4eElVQX@f^)d{f5p6YJ zlao+aGQ3gBl@{AIq$vJ*rwJk(W^+e@Cj1(B+uml|$w7M$ge^ZkKd_8^AJ8E#(VXT4 zBgy2_5C;w>G&54iSW-USIHqyMG?M5l_Cqcts|amN5yt&-=-DNmYG=H#ppU3+9o?xa zO#Vd51*C3{J-_36pR;Wfp&}D_?@(Q}vWd?nmbK~eS?`-&X5QOJSni4VhqKv}SUrt7 z=7QBGW)1A8uAc1h&g!zHk27n{Oo2x9P>+;PCs!rGnn3{X`7W(bP^rs!rz|;F0ivvr zHBpZR>LY6^YZ}PwBI}T@%*b=4Y=tZn#jg+Q=z$Qe~^@l)zdo zMuN@nM==4J^UmuB1NB~9>R1)0PbzCUCeCwr`*78tTmBO&@}&P}J}m~{kG%AhxeVuaq|DR+l&)OpBAjf*3(jU9WWvolhF$nAR1aB*|sE-J?@c1TXmcO?8b(X&JF4Z33u7Z;(8gi#|6=m-cy&o^S-{{$?diaVD_`P!3b<7S4tN@2a zxdd)?8COo41ailMW|+Uc+>Kx5wt3A~%#wu(-?geod1c0tHEbW3sfeEF%!azIIFLwt zOmOyZ4b+&cfC~ytImlp8&3G*c%LpGq3$_ulmUhpd1G!Xgk<=`A4pM**j5>H#5k0Yf z598?m=k>eD7o~Ttfu7PT)VC+_g6k31SreIxFD7H~XkQO4Dgi1| z*#Qi-@a!0!^Fhg+vW^!lUU&mqpvZK-O#4Yb9Xk%^t|hkQ|I1TPdAR>@hh{YJyfgK_ zsu2>!!O2~h9lH#&9vZxTq3omVU6M1tNPKffEI6p8HEQo{zKsNS7r4=h`Xl9R7OZA~ zcjG|jkr!YAt|n?p)y-HpMczH7N&h^flL;cx6Ex9V^-t}rVe74}fOW0k4Q~*Vr<<|$ zTdH8zp+P|)$5(nUZmnet(o13ddatAmBYf8m9)&M_F59c_wlI8v%EdpPFbS9}Ix1Fz zpB#qxb5E(XwoWp^AAzQq4PHv5s;1FGxcGurR`H)X-kP2hiv^^>AucV?RG_n-;o0yD zIuj$LbQgzb`k`W(Yl}AR>GHWtM;SOI1Q&S-5;$4qFv}V=t6c^m-$$ zoHwgmSku5czC$2YM{RMw1nN`dE9ipv30^7gIZRa@kYcSE2c%9H-DDCxX&VaV?+|k3j{TY{A%-AL$w*^MwI@F+J z+9*^e_7cV%ya-BpTAuE1;LqF^eE}#anrhY0=ht%-~a35 zx4|!|``s5j&9nA26^91Y+JCMUGnau60`>RD8*hpdj7zc;BRd3+OV2>6~9V7WeJ942IgHS#>*Hd58JrZnz4BB+^5BP#%uHwh43lAew9AZ!F;5mV`k<@J%=$mjIZVyVUOS4Z==^&04WK;txj~C%F1y2M4(v?rP!ZWoF^-HEZXQ3(VE)YhA#j- zaU7M8+uuX^HvC3uMIk70%K#(GlQeS+ZuU1v`hngFLg$pm6y55%sgN$+UHcK^rp5sG z#5^Z^L2~g(S4BzkA&4F;!Et=D38|#dtSU73lzD2^|M5!t=5o+xBg}-+`D_kcWHKXT zTFDG#lT-|8HGIHxC6>EM$^R-qa@qc>g`&kZ58q>%-k^}|aO7L_#W5=`T$qlsg=IKf zu#ouV^E@pMGRE_n#bxFzSzeXVPF_M?_^PlaGjWiA;BsGX-=zbFx6}Quzm$Kj=i5Z4 zDX!q?&=1XeEFZg?u$TQ7lLAeyYK;ZTq4cbDZ9LNC`>id4cqL?i0LKYeWz~vs{^oDR zP57kEB2Oyyfr6{pzbx;t-OeAu;CqQ7I9}A3IX0V?u^AVxe5AFz0iA#cj%1*2L5kMw zSu1x3IwJgisJoMoFpVpJn~l*S@FK5Z3O1`C+T;F&?^s7?;S}oJcF$JmVp{yxc7jKf zz-STib#9)QLlNgUgjo#>tj4_Nvrz7iOM}RmwqS@OhFC7CAHlLZ2KCaKV7?+0xlOmO z(Ka6mOC&qAt+0UHarpdS+Q)k%N^zHdpYvw#Ndl-7XB>T!N#Ur!l=W7qNxq zrLfl1{|x1EQ|c-1(>cf&_ZjO|HE&w<{)fvM3>X9%k*WFzst|_;B?gqWG*kUrY=8!5 zj9_Wt(%Y-IJ^ag*<9{m^8ae7A@+)o3bXB(`apLu>0575zY;6vMr&;~ta?umc zVewy-Ot}aq@&+996Z<7YR{FVVLcusjSK=!kXhA*K(4#j;7b8N_7yjG@$|b#U5Rg1u z5tKdWvRhI1&L#icsPS=2nLRy$5RcQa7=~08eRA3p)7r7o?c4+1N*J@qA>QeP;px(e z4qbX>v@Op;5za2kqYs0IouZ8uzzSBBtRAG{{um=sY^VWQMQLoe<^t^&samQ|c82H(^RpqOvs?rcrZ9Erv|8w*Qo2 zUSLR@6F?#Z59|ebkvxb}gs>!$xnVekive^^@RK^LxD+-~oc(NrW{9)zhU{@RT{a+@ zC8!&go;ET!Q>M8Nn4XRTaW&$AA*P+cEe>(EY&rPvNx+~Y4Hu9k{KlWr^p;`N8L!d? zaCOL~eI?D7g)W12Z7j;3$C#a1e`mFQr7J2x;C_7?_Qe{(V(bTdybSYSetKCO+?Xiw zlsUjh3oElvHs`QLao(1R?%hZ6BTTXEjc9#y%~ED3iDt29rR+h2UvEZ+{QD8jKfe83)ko9V;H z*n>xXS(i>8D?WR@k7f;A^FDEk5i{8BeT^peL?9}%DY(n==d*H^s?u$3C@L0Yf6$}+ zgK)iQVp{jN9tCZ_h!=S15e5zc*g>~TH0|AD7Jk01AJJLmr03QTDiK+zCM-;%U?1{r zNdo3^g)Xq(-%dVpA5H4Pi~}IZ4ffPmiC6y?7Razt+GlI22^gKyc3fM& zCBt#u14G*4Dy<0X{LY2p@{hlNfBQ0PAq^!Qaxzm@t zLg*QPx+sCNE(K+H77|+}$sc*2Cx}*5g|g>h&sMqMM3Qfk;LQ%-Ff8mCx3M>mlfsi&0rKW@55j)Wcij)n{4C70^fUPgLRSP9SO{h{@m)PFvg5A1@ z?wq|!2l~cLMQKOSi)Azg9qXh(Pe2j-I~onStw???-2L^Ta1~731E+aqPuOn5py`Ef zZ_~%QrXjOsf#FpHUD^_5(L}?(^fQ^nF!WZmik1=5ndsNiz(+qIsS#1AX<7jp=h4-U z=#X;*lFkJLJfQ))aIDqTPwK zZ%XGU4*B4C>b7B@Bc(t{f*L|0<`u|h(%&o7Pg}igh!dIZ;87Q?hPzmB5wS0jm-~+$ z{lV-idbH2Z)Ah2i-7whQzziUn3|o~j-x$AgVGhn9y5&vLA&p5jmpE7V{R40r5J-5k zqvY|s%-6^?!`aRDzZf>ZDx3u6&v(#}Dv8rH2|Sk&omSBZ-dy=AW_qy#y{xROn~UIT zj1&yrDCS8h@o!dV#YP!CNu_%oNP54M;W``q26d68SaO9SR{bF959xzD%(w_KKq8T* zbq48u6fbLBNL*SJmo$KTdnQ20WyFP{Z(& zg#ur__1XgpGoAs@w4&l``H|e?b524FaBIbv1H#KA00z|VDaVNo&;`X)n%1_`e;?KG zI7@+57^Er3St(nRkbJYohM79GN>(Z}5VhMZs!1RUHOBHqgj67mDu?RfQOTM;q)I+y z>la^(3RWy79nA*7MTb(iE5?KGjB5!!#*-Sy0@ghTfYazYPZd?OVVUgKSwlDx>W1hvR9elpTagM*(ao2FXGoEQS?n3PAoB=l{8 z)ZNEQm0ps5lX@{iC*y@+*WdD9^qsvd*ieu{fO@dc@4Vr=Q7bN87fvQJTRaHtpa=&O zpHPSNlwV>z3rIY6aFf*n;K8+wj=7edBS#~$k8DcBOOLWLL2*IZuD4_2S-O};lCL!s zyl1Ak`de1Lg#%MV-$c5wOuUC-hV?zaK0As5QYu-64*@~jA|#meYcFQxZUsla#qw}V zvACGa6isPAkP0CIWgy*08Y{qzRk|FxaD~S-q2$P!MaWNR$Au+%1-oZsP5#&oc)~C~ znJ@!vH>GaC2|xoV$Fuo@&HbIqULAj$rEts9dUcn^>S;8?iXmK>_C@KXx6|To0j!;p z4*fAE^CRGSlX|x(WdRQXVM+!>)hVl8SQ=xl%4D56<4@|<1Az}sV~Gr2l&5v*>XbV1 zZ3t>L)5MZ&4$QiF{%R&#${q{G_>3<{k~5)T;CXIKYOE`% zzAwohefjes&QTgB)Sa_vl@zgrN|a?2msXFfEMx(YFJwB+}3(Au_$gQ47ws$cs4L1j6j8=^745*G-b8jHNi7%#1LJ5{&564O`XT z?_!fTi`!6U7TX0pSafzRvDvj{?Vthlj0u=9UomIxYeM%(P|D~zy|_8SC71e9HU#%9 z(|?Y0LAV^1i%dn9@e|33kZsGo79sg>E4u}DPhGi__(J8WGJs-D=@@>&|E-rM%Me~w z#sfd%Pw2zgtzlV5szyLO9u>cG<$&zKS)4=75r&-Gv*OgAP?c1x_)ZO|)n;C?FZUlA zu?$&g4*);)a-(fp_j4tWjJ0(_Mv*Zq(2)H9|J0YN-*Ms?X;J_PCwEZ?GOJ~M^|UaP zd;kyIJ+W2F5#YjG($$=Q^(v;M{sztloTFJbN4lE2q3NI!&HU#zYK^}Bx~Z4TNgKBY zvk0|mzF5Al)Z_If zs+ype6u1DUd=J8``zSi3kff0m{kN~BPEkxibWzz939=}(DGysFpZ`d zxng7&>9c$Qn@dOIz@6)^SA6dX{7^o*=maB zPikwLhMn6{NBVDc4@2c)ewcxea%wlW(+dxD>m32d#yIl^Q)nJY{@zWGB2PR}@iv<_qFbV4`rdhnLfW4aLNC9LltrLTCy`4TXFjJJ_ z8Vp5qHvx->L&QGKChk*)6|I>K>LKMe*MbQ2Y$H>f5am**qu#2!3auZ5#P-Y2;#rr;FM|=}odPL4fSRu}PLXsM1O-9brNa`DNf% z%FNbB;hc)Tm0u;334IVH@M8r1Mcp(ur2zh{GFBJ%Zy9N2=F64i3k9GZN(F!4Bup5awv#ezZ~f?U_)K*$ zfzf775u~a2qJO8#{;H-RBUrTs62i(1coECaLl?zKP||woM5-w+ZmE7>XE#D9ce;(l zGZ{A;2+9xyAHDcJ0;Bifu_x|39Y*D&bqe`wl4HN~&~Tl~uN}Q$enQP8QwjY1PUrdA zxh*a`NX9Vm0AOzUitvab7Eww>0yIO`BIA+C`)0Oryp>%Xlke%wd?6F*ZB{^O@N`WG z%%DtZqgTXQ7|3u=ONHuuDXGWj@K@Ze= zXb@C`&&YM5{O5f>dwJ@+Z--V|Vr@OAZi$U)=W!a*6}TAh+F!V(?OPiu8yTZyiwB?` zIsx9v?=7-HY)^iNt{KE`#(zTk={O-5{)GN;Mp+4vR8+B=f&Cti ztkn${OIZ9p#UNibEJj@E75`3sYL4LfK!D$a>LsV!c=fpVYN}DA59+YWZ+U*dxdM2t8R_{PPjV4UO2v+y~4bKMu z0YA*Cc1KmP?XJe3rsgjfRyu$kIOz7`Ipd&$Z^i+;x*v|DZ=}x2>sDh5bN*#TY4Wb& ziP=rd|6IOGN7~*@(3s0~z$V=zy!#e|f)4s)Pu}4x5klXOB=6ectI$?Y)9s(?p>Z>> zB}c)(Y=~9FHH1lxzHf47%y9>^@f*&Nstocvb>+J>8rX3~)K>}oFCFrsB^f5=Bj-fz#0Yn%wK!0(G zz5*5PYd+^rtqa)ueak2f+T;%|>B-%6Zz{m8V-Ku^>F{kMhE<;G*H&p5*)^~Ir_Q_> z3H{Z{78bmW!$s5~jSz$mCw321{pKH+L$i)`id@zc-^I6Ss^D&E0`#AL72_Z6)&Q7dbAL}-}xC(PR^l?t(ucMR`nSa|XQfUqSb%AU0~ z8+?2aX4AI?aT+<2!XPrdg|j*arA%WkB5jGp)(sn)BUY0cr4_`|7wsT%Z^H;)7zB~i z7ah)azZL%}+|XB1l%28)hrr#RRDN_)n#WkK5x+(@yUlHlJq1`^`EV)D;0du`8s%u2 z?T+Ng^zAe>ds1&(hpHWA;sk%t0c-tVb;)+`7j!01Rh5t=4MjAn(M3F=q#jPOWr=L6 zYHSi34vwR)qa()m>CL0QlT1v7d|&M5dg0;K9Vg7LI^(3hL1jgw9(P)CzHGDOQuleU z_ncqkk@BwFG)zH24@*o7G1uU=in>^;Wf{hE@@Y|?wb&R&-o{2`olRI;l$US~zvEw} z&=FwG7O<{_RAQVEHm`RcyMjD2JZ7*{)YedZgO%{8qYILVV_k_Z^@8`FI&-9e2_mLq zKf*f1GVThPfU?}CK-y!qG`Ea@9F`iw*fQ?ej@bAxh7dhDjPKtCQsl~AFULD2L!q2#VNpN~C zsSjmX>0#5t1JTH{og>`w%Rc%RWy*fp$o)uy0{n(2Ktm)=66W7`AQ>Vfl1MXAjt32} zQp-+;H{>!RxASMAxZkfZ#>yLUe*Vwx8CUH9@$xReNjx$QcUNRifz^(``sWZWzt(Vz z`6Yu2iGGiwTD?N)&RJRAd!yY=LmRmaB&x>j=+JT~gHu%oy#0r@3CR`aZM z1nzc15I!sH94edR$|R@sdYaoF2NN85)-?Y8E8xIfx)WwhZ$=54XgYHBy8BIb6Szob zi`ySICpPk!K0b-jpYDp|79^7Ikn`>8j4L?^-2SzU9Gt$X*=tXUnXF|@dnYo<+E9ozAibOiR5+ z)Y!JhzX=J7b~D>63jpBg!cg3pUzH=SAvA|b(TTB9y|3n(8uh6apwcj)N|cg%t{BgT z6wQnDxpG*Xu#mq5zCv#5qC!{Qf@p^~nw;%`*o!Q=qI&mqS3Gi@FQb5u@$+^u6B!1F z@J^Z7mQD#Nqor}U4!gqGQ+d(~y&VTt9fWLw3e{iYqLpcs#=x4wuCjsg8WL*ULsympCDHEXO}vIzfwd zjpS8E!7aM$$3tnm8D!hBH48v2yZW!C6zd6TF~}$FOe1Wn73#(89SBR=c=e(at zZLP0TgOM_UyKA^MASc$GwFmv7`FYSY=?SXg_L%dHqPZWhH4AFV=)UR*T{L1e3^Ajg z&6)p-9D-0y-1vc5il9$6+lI1!+PmR2m`vB_aP{xi8RrSTqi{3=CyU-0UTB(cV*IcK z21bCz<&c+AV4A?vy@+O51|$EGeWqZ6Z&)RmiRctVun8Cjt&Xd-IcGJRdIcb^F=@C2c_+60e8$CK(lh?eImGw522-i7p_adGI%WVt;^Sb zUM8?V$q?OoHG1s$Tr@mQ6}Kj}Wx(LPBo2cjfyglMQQI#D6mHgk(6S`q1y_(DGS_33 zi_2HN3ddxS-%y1MzZzsl%=6vMWD4$TrVvFuoZCEWL$8z|h#8i6@)f_FMZJHnDB;c&0 zS7H5#$4g3k4$|@vna*T-xm)XHcCu3rFQF^hwprAz%aGG@#m(t3`y~Ae)wS%HKk!}( zT^?P<8VurTDDN&26C1mGK|!K z-wuMM$5b|sle*TBlc;x@S(&=-U8~8|n@x~@^t@E+wh#4l8I!g@l8QuKsM7p9cQ~)N z!5NhtTj7w&G{7A`nW*UVf@#z|vu%_;T_G>XQhO00P2^@g7;f>;iTlu{3;@0ge-ZN& z*F$=uRp_Z?1J%GkN@e&+e^)9Mub;$A9en6^>0*m6unMHY<&ZyJsS5Xh;xyeD6kVz&ujQ(?VZOA} z+};GGIA=53i)lQt|6kqC`n>)+smh0W_4~-74uwxpR0;Fa_Z~lQx1oP<7rFh$S=y~w z^D9<$uqzp1@)NqMf|)xu%0tKus?E#=U+xTeC)Yi^x)J+yX?W0)Tqr$}9xCVcYm62A z&Qxaf{$+~#z_STJIU&U^lp_;q9(jB~=N4U-P~tKSGe0NJ>xjhA?kjQs-XD?V5#}z- z%Y5~M^!0pqRllMNHX(3+nPrixJ+)n_DwbBxn8De+y6yT1iRt4<{nvqo73zK1HsRrF zd5AtZsA4Bd?-8&xebL;6DmQ5zWbTX=y3^Am6NXVT4 zWDfGi@CG$bFO%z?MfT*I7(Ie_@%4N;h`!dO&W`5JaK85zd+;h?<}L;oo(=s+zk-~v zOIx?uSrY=cWztW0EhqxS_zlwSP)c5A@2kmRKQIt`F)e~G+9TPOXlzVGum)30o9uk` zVIJ?D`5Y5V$volCEsUINyC~@kdOwv)ierLq9n#n5e0c%pOyalOIr2Og(UrTMaFkN? z8^0}oO#wC^{2120W{6$x5z*=Omeje4Y1$#J%f+JxWc@{wIUNrZSM#&AdbO~(bNuYl z`1enp7P%tRV~{ISkJvV0XutG#U)VorI-h0xC5z^Cq$70rp2|x}KA5EQ+;mIwh+sjN zEP%a|3+0$hVwyjV$0j3@!<>p2Y0a4((rC#3Ud#oxv`~*oW%nwTKvWaap+NC~IVt-q zTvgqHaBLrU_Jar#S!x5+^FZ@8#*hJm(` zPXXE64M)cigSKWOAl6HlhwDCMR;qg z9F&)I1}KyNH5Y%##S#u+8s$7$GG8or0o2z9@$ZzrfN;-#d+WTeddZsyG6N~G#1sN7 zF>f;fe)y-UpMK{IZ+f9b2!o5jLz>OdY-&x2*Jtr!48l@Ao*SpRrdjx}@| z)tq>c#?c`m;(pP8S=OPD3pM?m*;~uv>-Q#V;(v0#(|7VA>U&f`T5HS5NgSkby-oCl zE+o+(r6;Bb@MZ;&Q#PrAO_Mn(qg+KfokIo!S4@7n>9fuZuq@C zQpy(#kC6J*bFkMztd1z8x<3w5^gqzxW5*%vGTdn?mV#K~_D1R-d`oaok4FFWE*tH$ zUE#wwo&>$`_RR zC1YrB&A|*S5AuNvGI?!doP#bGKE1QvIQ)gWD1KqZl{`;{hH2OX$A$nr!=}5k@hZw| zMR1S|N{?O+7;$%0x~hEdLV1S)OeX?)0)Cybk_R(M9qwq1HuJ)MEuRPypJ+?;ac~I_ zaw^b-0eJT~qOEy$ENZ7h_}mSdG%j@_m`ZC00EY>kvGDdN^(UI^I(IazqeiffUZxPWkkt}(>~e8WR~j!^S*vW0j@(WMt(6db`s7k*)kM>5~Le zWX#xQKQ6=U)FctUxu#Yha&f^QV$@#BZTUht{Ei3tk>iEbcbvDW(v6t z{9J~FWS6!txa{YbNtL|w~(CngTw%K6>PaPS9OYW5S zxL7gP^SDA$E=vJ@WbiwFjHu!lKSdTM-7D;%<%i=u-EL8%sc8FD_+48g@1QghB?W_6 zC`mn@m`sEZ=?TEW{WgXH%a!+K_Q${2WMwFr30T%1YQKLD7CDFSvs5|~m6L^KWRF8e zgX>a6T;ivD-8~!jSG2Y6EOo5)%<25|gQ24B+T{tVv%k_vsimK_IZ5meY1m_CRBjYI z@5%zBH0G}IRw0jbDBK=--@|jwfuw-yjb9m@UhB7S+F>ZfXC*@Mo_m=t6GbW2m;No! zjl!X2Dr3F6otF)UEONLtiH3Yy);9jaR0eD|109d*i3D%F2i;yI(uHJU7A8K^=M6zg z9PGWJx9bjWc6#p=%_|_Y9#52H1Uh>-?8%!NtI$RV&}%%MvePje&GX{~B-qzZk~O_j zu|*DDSrZBZutY|l0z+&1+k$z< zhFu^X!qoqQe7exJBv=ch6gmGRBi7Nj$lH5RX&9oIANpJt-5^P~lgyeWy_~(b;=&)i zk#C1V8>{n9V(sXHt=qdB@&$*Fju6Ck2V?oPZ7d$U_zaaM-?IGBjj?XOS|OfdnRt)v zx!INyF}6dv-*r==_u3dpid0>~xO;Q3Z%kEa|Lcz(B&_x$WWh1drYw(tBF9 z*3>2ETkX*NR$Fk|zYZe^!yc%}SBKWgGX$e(SI^`$L=r<6X9}A1hXC)gv zR*bVq!{b|;7jb7q^(QkC3C#+2ng}%>Tr9l&s};>i7D`_?pPhmRx;cH0%fequmQEwg zYEPjjk?hvNn^!D(oIp$aGMaa(KYOWREcmltemDJnLkadld?N{YPl4Fz(RoWFbP<1V zNI9#(I#v8!@apjVi^lRkd_G zLkNEC6wZNmj5&%4Bgq*7hE_9Y4dHwDb#B3i);S~&ewY~a1x9tye5rW)i#qT}$O>i&*iZM7?*Wu>D_O)p+HwzQ-1}a^IT=VUra9xt{ES~gz0sF$5pwlR_#=D7 z;YOeN>vPfyezDae-LQ&9He~Td0n{wTX|X1%s~95B*5(Yk_30iJf}GyP!QdT3R~qGU z)FhA5rmeojUAnE#p{e-83z54aXG0fkIstG)@8`PnN{3v$AqC+Cq*2 zoa#7h1LkmW5N*}MaR6bVQB#V$@o7K(ltkJS z#UNC(_`=g80AwuPNC=XM5Yn21{PpT^_MV6NJM538)i#)KLom(-VTdVdaMZL0twt5F zn}lBbMAPwfZtd1uXAWvjaU8lqm_lsX48z^FS8i(<26f}XB*wa>1HYk7Mc696qF}N8 z{NA0IKbcu^B*`D3-2U>FqIFO4(N&4JP>AfTWWaB-DGVxJg&%-X-nckHL6H zEUo7&h5$!5B%f$;x@AYH>baV;FTr!jM&fRx7x%nv7WE9-d+rTcCds41R;h~$ueX;Z znSlc$FJ_d_lB0225wOqh68T&t;Y*C*RPb4>{b?n$Sci(G^rDAXY;8dx74zX?GV4j~ zu&+>f8*PT?C-7sI_FdP1c$TIW$g6Z1*qX(RH2SOu8|e71I?FK6+o+}2ATxuYNKg$d z`by(cn1cmbGBkYnvav5UGB9sfVD6+8R)00v(pfz2Qy<{uohVJXeR&o)6a)zivsxdG zq=M#a9>XXrv}1M;Z;E2m2qq1m^bOl@Swblp?nnj>h zd$~Jb6gTvCv|~E}(w92kjfB-??qO4h_R?>>yuTO4MnZjwBN$cpQk+adF!_ zq@@fn?7MR(^~Y3hvOb`4BG%{2J}U1h`fE^Y5kpAKC1z zG?xtqAf5-jap4gadk;-uAWk2Ki9dMFSg*$tKTuj(vqDeYd3@KGxHR2OEFLe$ zVdR$use(o$*KwDWpMq}Bm&a&W8VkN#`hIHBTfn>`cB3v=cWwsfdn&6FS<@fe#&i~b zKsO2ICt=<>ca9x>*_m(2%(>mJ{JtAbhIQFCAZ8rrU?DzArFDeh!-e5zfHwj!1&QwS zO&MyZbuILwD1t#f-+JNN^`Bwx!#SVM#(AZEjujC%l8Y-+~ zS$SAOY{tVb`~BR16sp5LjWl~_LHNWq-LNlag}2ytd`oOOXz!V%Kp?M=5=Mpr|ERj@ zNxazs!}`L_K%B(H=nPd9$G^D~AIT&^bRRzHW*T8&xHC?yTAIBGvyZhYKy6ecV-(Y) zApv-=b~C+fw;lP}*82jQCo06P0;11T!5k_h4EJBsbFl>|CQ$0R9!g`E@5c052rUrt z?bcz0TsX6u4d@+6xl?Tla-CgM^!=G^qq7@WV6>!0=0(chQCe%I=8A4VBD z;-+h@)^6V6O#%0i3j-c5AatjMJ63Z9_@&EKG&iP7gbd88Ja6J{1Vmj(cvC@c&ER zepGFy6rZao+8K|zx9WZZ=zC#0O!0lpv`&A<_1^0VZ+>{iY8ik7 zTnP8A(=M}N*Oo+Fajxx9c1gPvU`nOdj#cHEiGIhpg8quchs}-a=kr?TB;-N|h}YRL zeGN3jv?Ez&uBli9Pvy0g@YQ{%8E%tmdruHS*%N)pWxjqcEu4e`BS@YWmeP^f3v;g* zWoz0+Z6Z@iQ=p&!JWhaEF<1KEXz{8En6hTdplCN96_`r4obgEx-`ddDvQ<*5!Pk9Z zoblc=#EbRLqX6V24`Wc5+g)Kh+X#`I{3XvcJN#+JSZY+!PKLn;@KUk7RywJHDOJaQ zqi)YW65L}=1c=iXl+uS3DrU%;sW+dQG12z*+0rAOxEz4Oaeu7MY7$Au5hsfRPW81T zovU6=%SSuE}erM<;LBkXdseohYf}03M7EAkLRu| z3<-z_%Ccr>X7})Kp;4SdBHZ-vy^?4e9ccf98*oIGvQR~;$bVw+B1(-RO}PtuD|N^% zo#}4y$gUFWtT>k@G;;jgXwxp*G^AtH2KpNRMpsLjJ@;g^oVyJZ9`$dYuo*)p+8Jr5 zYv50&6y5n(@sl)I9avJ2J6v#tl1#!yG0PGl^2PWFSJo{a$i9a^`D`xQm z8Yq^;SIbA8SrAFUq!=4xdl0#=EOT7>T7-nj30G($qj(1FrW82EGpxwqFD3VFYx_vx z;toYr+s({ZjgB_J0)vDH7GsuT<_3 zbs;S6#E$asZ(F}mcjqZbE#5|Dzq{7|)ItGtaf`sSX>kP114F{4a!QUL8Y1RvcttM*xbrqX$N; zI&IFlbwJ9PfpdKmF>cx_edZZ1nsD4iBn1cRic*3&5>y7n>?znbZ?8!Mra*D*FV_hV zeL~7IY)OcgrW<+!J*V85r_C7RY!R@@tbEmAzLJePBne_I@A(>L$Cn#c2R^UHViM$Y z52KX@3QPL5mHLE2dF(^0)#U;&1@=Fgx7R9(@mTb!UiH}5_Ct{~%!SQco$kC<2?-7* zxvI`Tc3?wh##Dtz*4z$XTu&;70wLf`+y0rvt-$ZC3y6%)JmgS>FKkxaJ+P6Z*kB#_ zEH-|Q>I=TcTk`g5J%+La2W_`f;hrk-rZ4aLL|g#MRl-on#yZnc9YF7BjdC8QFWO5# z=E7S}?CH;mY7xKH3w>yn9y$P#h@Hi?WS+fE{1KI7hDTEEc~kve67x%xdGpVdeu`V2 z7o*wGGr`!yCUorn9T&D+fD|a@yQ`t_0-jgkN&Kp&8<^#(#;}(jj_Ci!S`wjz`^0~E zt?9`Pr_0&nDgWCv#M-O52M==xm~F25-fy5_>>Ce}z;n^n*7Kh$`LNv)Vh3>;VW72_ zKH%rmbCo&6675Mb!r-cujn>>=x@zRlV3}a6mx=WtQD1cN1Bt0 zdx=~90vX_?&5oIVavYFLRZi7o_ILA9jUg2XK!~9ly6Yg4I26f5;QrZdz+vLFRE>Wp znJ>5~A9sdPQZezFn$d=W!BjOXoBeTCkAjDVNCqYQ!e9awks50`K-+^V&@*->w36L zHP5^N{_1Pj?<{&G-%%QPxj%7D(Q418<&DCgg1Y5@F`KHH>$uZc^$5M)A#!I0eteT! z9_1K4-$lCFxI*^k*-1fdKEgKs!dxG}DvBoo`^s?qWJ_{FnKo6IGBaa;y6v|=lxR2= z*_P60Ns#+a#?cWF)Ol4xuk%*L4mnPBUKga}zv&`DwrD8!tLBx>+)-Jk`}7ySbCyU2 zL6m$;UDl7>F%k3X6ezI5O2!YPia)`iq(M_0Hfqu?!8~39`*->C`X<-sd=9P4Xje~) zyl5(xnC_5tx^3& ztQo&I9*3GfTtjKM`1RMBEL(L^kVasLzOHLde|ZET`NQoXX=VPemteSdt_on39C_dn z%ACyrGdJjbd0DfB{dUcABwPQ0n2|r;ng2b0^zwP_FL#aAk>r#&sVNF3AL#a38Cy7u zjJqr*^*5@LZal0HrYPt?gij7x$wO9Q;$X@It+V&DSA&Mef6KoQ+~bG1w}vF6RwfUB1_DA&DS! zM&_3P;?O!oPD-S4Ziz_EN=@QYmf(yBndcrs?v-pFcp#Rk{1xw0!=YCUnoA3r3)E=y z*vNAbG-$R$RtxvKmZ17V2Pq2>PcXoGXsma@Dl?F`*$|vwsTFW3VKWxe56J9!wnqR! z2lGV<1SEB3ne}>hgi(3(%!vd~S18B{7D>r1G-&qt=gDm4;1HNucnfM~bKQ9)uTc%xOrdPpcQ(OS5-Duqf&!lt?q>R!$XVDR@!KYK~TVp#lAl4OG zT#LDuqSQS^V2tj4+9>2!De-15iX+XX3gywdS?oZbjY2+mWtqIGou9f5Gq+JvOAeDRp7RdH3`nzMenr+$3jw zqBYr*2S7(WLt{zU0ewvn18 zBA(kPJqgw<(D=gU92}ov?FTVo?{ML9&PG(79wERvx$aZA*3~2i9Jd*xtu@I|#GFAu zZ^cu+!FEop2%kZYJ&m-(3WL$z%wcFrjf|=G?Lb%SG7B;4x&);IT5H}#gR6bxkWSfv z%gKDkL)&!4wnpCHTdq7D*?31ZaO#X!srdu}{@0yyCW*QDvFgomqRG?!(*55?r8$(; zByP~P{mhbW&+e=*j_6M|MiYQE{0tNy@%_-e(Ungj9Z65y^oCsTG&(!j_R|XN7!_8D zXu%?*O(nC8SJaUEF0|Ks-`rllw;w_O@=dWJzfu(HhF%fee~|uY9mt_8QnHg39=k29 z=iC%c-S;Y~#uYte=8Q`cM(0GDC4HGAC__koO7$G%8^B7Yijnh|fRLJEAG+B%fWUn^ zt$L+ZG(}5B#L7xBz9_vuFe0XyHWvJrf5iV?vVvviXtd%>xlU>M`>^i= zyyWL;gt^2moBLt%D`JL*9N&tHTX5ZF!}{HB25>NQ8(7(A(I)?Z8LYjvbc_!*;%S@l zy&DKpqChiqfx&o*G)D1$FN_6M zM77T#hL4=M+s83RIW7vQ6ECv=O5S4HM5A&9WVB;m`i*9y_(`e?{A~Ghm0)bJ5>8?M zUWQ|-`dJ2L@t}&Wgj1q_FkHZZGo4z?LqKl105K#uLp-rDfSRR@bzybVUn~ikRB&kp zbJxw5OREZ=5Fw6@*)Eo@&Lc;Wn!5x~$GV6^-f6x?Ag@@=?gTJ&q%uceouLbJefAU_H1z^Pai+_H z;uz=8;B~@zjB8hN-8W1Dz;Z>X+{}fVl$SoYL8)?4H*HKNxDUk@j3@$ zU*{-{5i_G9dUgZAdCF*!sq2P`p!)&fjWTSaI^DQHrO#RV8Y#~ z?ZEpTxPH|(Q4@Ez967fvyOm>Qd0DaJx^DI++-ux7jhCaipGO6Zn~P=Yw$h0AK@2v% zR!_qe%T_~?5#})IHY_H2@Q#+&-QhW_el5FzI0dH-YyEbGNS(o-&N9e8 zK+d)k4Z_yR3BL{JiQR(eZw^@)A-&A6-_CP4$|Gnwp5IJ7;g)xp`}#<+}+(MT-w$IJ(>Fh*9%RAZoq40Q*m z%q_9ge%L@#6NIG+Vsp6US*OAi;%+S2{ePsRN<-@)A?D=?@?c{sXOgV1rbo{asejw! z2Z&5kt2Lz*ug^UoyjwxJxQ8Z+4yNi+RmLy5+3_*YAg!!RV4?`}(2%2?m!7zwcwwo^ zfe)n+bDi%W$yE8LKXv1h0J*S4m$D|+cI+7)S^>9z+^Ztbj100(V~TnXn^aB9;GDXf zN$7zD@2GIUloR=71`X49gF0J`z{EISF%?dd{isk`R#t}4<-AzO!9Pw-gK^nC`h?FL zTo&Zg`g?PCJha<$^w7F+VX771Lg6=x#*YVuXUc?b%+T3Fp%sbbD5&XNa5%NaP_1Jh zZuw*6dmKCgN`f3Wj<2IQ0pM9Imc-4yYr21COP)eBS4+gdGwO;m-DjAbW3CmYb|C(as3N@FeGu%CpE8YSQeSFW zTCc@Ff4~p#t$j2vXZu>a8Jb=!A;u>arl7Vn38Ohi9%@OG$<$ef&Fg_^IadMyC^iUE{A@O(!Z~Axyhu&@5Dym867> z@k`2pquqH2Er25+$A_%;VLGknmI|!{4Ba-oaVC9i)U6ax z$8;7Z^<6P$8$|+SN_%0$BGdE?gFS@a{_7O1B)a?Ye_riz`^KL;J4or|Tld|neYjEzDAJQ_C_60^y_#}F+Bs%>+nhVaexKdJ z%8Ox3s<|HbAtnB?U|ACLOfIvnQRXwLwDzC0eWnvZjk$vYV!V2gRs7J|Mc)SlzR`u1 z@p)E;{+bE>P)Bp^4n*7FBz^He2z>HXYhP8#xf zDxq3l-~Zm-7f6kOb>eEjH?sk0SKNJvtDjq+!lPVfSiaA6W-y8igI@Br^oSG{2z3!= z0rBx)()q7)JMd}%L^XS%L+{A-`A1z!T~*{UR72>j7aUx*zTo12e( zJsnEy(_tdxAq~|MCWCZRhLT6w8?2M%8dlP92$pox7DX~@-H^0ugR`cm)LHj>PO9@X zQ|bO$O?Ifh|JqRa#acbo+SQllN8cS&@V1cFV3x}8VxJT&SiDNryr$O)Y2Gel%D?Bm zOCu{HeSSHX)f*la8NT>0S8dm>>?jqIt4PFm01~?bph)O4Ob$&J$8wll@)H{LM{>K$ zE@6{jC1_v1tBXM*g{tRzZhsONS%VG^0$Umb^V1*8*or8rR7?bEVqr>>IR^48J`f~i zh4_0^@C{Xv5pQ`rYuFQ*p!|fz^FXD92iw=*5y)!RxpPMCyWcO7{Xv)vO_HffUxC`w zgP*(=$o=&hboBZ%{!x|jfX#}UNB_>5?&$&4Tm7T_WttIm=f2`L*4nl_Jm<}-ypjDJu3&$kysLGo;R9FqKdkerwJ?@!B7Uv(3O4YVe2B$T1DhY zOc$1(5F6m?hsDA48-Bgfn2NZLUZS9yB0$;>ky9DWJjC=y&m4y6W;sYci2yg`w(^Q; zj9|O|aHCLzzP1)Di6+-=+&BaBwsS_p?}Ns!%KY5u$g1@yp;u~;>X~$}9!5g1@_mf@ zmNd;8?EP_bT-8+M^1ZuC0%Z;mhPLw%32-o?Nx03P7uVo}roIz9i3?wmBaW%xRj@ys zqWE&hb2^lw6w?$^vPac1Asj38fg)E#{zxFEVGqLA4aZHoBu;+47ztJJ@G3%3V1z;KV9spdt-{>SL3s z>nhGysTraR#~XBMG)gudU!!R0|JhcP^u;0pw#cQhQ^L!eiH-PZkX4cqugQ^^8JFyyRx4$!(=o^OEE?G^>L*6dvf4zwom2TAGcae#gs->4oKhPJCvB0vcREc0x+k_ zgaBbCv>0aWziH8*-NoDP`K;~zwLeZ8!KV*PMdud@vYzQXl}P*PObPAIE|Jk&c3R1d zjNvnNhkyqtec~QXYXY$X+_@Olf#ZPxvg_ZI6UH4G=KA)ToPGgl*coYSZc1t_HR&LO z=#)(u>7-PPW>Xr!f_$RaR58^qdWTxtR&k4<+g zIjuNqOV}(p1a_G{4Q(1i<}L2T>&BWG=8b&GGCeMxT%1aZjT*X04%o&M2`0`Tpm@An zWeH}gV%cw0IfRX05Xv5y8d}c;r@kENF8#ilNJo%%QR=1pnAnd_o~An3Mf=;V+P)Q2yzivt^6p~?2`Y4Zxy^)xOgIPII&^itV80wQ^|EfcFWQ^jgJM5 zv1qs-md|FDO#F;fpor{edpWYhhw6!32GY$2Ct=Yj(^cP@*BMVt>Q4bJPora{WqQ34 zYMFP~87>n@4@_gTN3WMtC?=`0?(~9wGsE2CXW&-;8%;nP^n3F#K+YABw_ZwcCq5|+ zO36hlu?S*;q@Ni(xE6Ij>NsSs-Qh?Y%K~cUs;{6YrvQ$EDMb_f`$NYIScS$nNHNPc z`~Cf)(5sE*ys_Z1jON-%5pOmSVtbVOVbXFQ@!D|PT=PAX!G4$!|6;Aw7QS{dfm#US z$Af>@;_asET)!wLcG;o1XycOoTEgNDNtGDkRv~3?1yaXdVOXQo4gj2W6ji70=}(z+Vszq|iBu|r<9zhP^t5E_ zTpOF)@nAAUO!Xy@co9zqkRwZNVLgL;A8&EGd})8asYW0?B42j5W+No@JX7Cfpr8U% zt~`Ek-4CvV=7%fi)S`s4PKkcAb5S1s?No!<&WFUXqe%iVt3@4VSpl%dDCVY-(%PQq z!g;)&Yr`TOqu}TfWVg?L&{s#z&Qwu^LNizTh8g@}^{Sthd&rt<2Iq5rfK3+Gf~_hB zT60B*Ag`)l5B9rOaJ-KcNz49Mu+>-WURU{{WtLssLg_SI;-gU$jx(kA=8Iz10~nv4 z4@T;WDTWgI+wc&4^;q72gMh5E|97>;^Nbkl7zl`!_e(!(7c*Fyx zd>W)AClWun-ICY12Yu05SAvaer-z(hjP2U34^5<%3n}G6V@s@n8pc}MPS>UFvNtFQ zGnTa4N&$Zjhuory1AZZx`Et;o-$zVgROG`L+EYnWy)}w|0S0;Qm&e^Fm|jjMev=~- zhlox?Mn!XZ)3S6w9jdm#nR)uK7UjHraiDep7Ok54la%?anr@fDl!nEavsmu^;H+PpKDrE{l(f8p(hv6IJM^Ch^m2!~UGrb>A zI@`!1lVlqZiXoje>Hgjm|E5s-9-La9&2~Cyp|6xVzhlwI_f#0)U%Zi)2huRqM1q9Ydl{Lze|io0#Yz6oSUpfraHv_Fu%iS`pc9XF&JEohQN1R0{s3&Z zxPNJVEZp7pS2+Q}NF(hrRdykFl9C`dpHB^G{$iz^tQl|0zuZ6BN{SI+pRgEjex3)ih zB{%;>OdS%Wp>>DP?d?iQg^Rnr0k~<#0dlSFWU+b1LUsg-C~^umjUh;COi<@fWe?vS zo*cj`eyS!{Lj^VV*YMAxioM=g)B;a?+H1B`2{;l`V$;BYUw-2yH86f*{0c#f7cn!n z%El8#Lij1M5J+6nIhB2!I1gMa;@oBtRpSr4t(CRPexJa9jc9dr1UJ6mtd5L<@V?a) z7mmJnaB{+hShdes?E>d_WKab?r;`9YZp211 z;_R@xLr5eXn5xY{a%XR0X<%Ui?C;wiAudD_G!#7NSCLTGZZ z+E@(PubOt;=hc<9g{k<(k8+vHHHFXwisx(=k#QpUFx?gHG(UxBhZCwR@sdh! zV0c00Lr)DSXq_*%W@OWy>!a=|T~Q61@gttFN?zQ{c-A_almI6~$vaUxZ;dw9)>g2R z3U49_s$)-jIG~9{kN*Gz83a%EbdKr4=0y`dFa#nd8WS*oHtteg$OLWsHfKzOo?cU; zf_6;MfiuEHwnf}~R&V-qkn5fVJw*uJ`?HZs9uo-7MdO!u1r`A{18ku}-$?t15(eO{ z&cAP@LnZgh4jKqa3nDO^lEIbN`5KWNie8Qo*l%Z|{|VJCcrQRLTG+vY3+AO#Nf(&j zsN`(H9gv~K<;+FJ6!!P<(kYGc6zej3z(*(o#3WJ3Kpl+WKVv$w^Pm*0d|08!9||C* z=#VS9ckF<+thN*8r3OSYbM=gXl!!VELrznB&&{5*Qifc;reiv8lO6}?0R{Y?`JjH$ zf;X~mj8%gNIo%IAfrVnF-Y+OCHVXT4(0SVt)h9*&MczRsj?z3>^xfBpm0A4E0@GJJ z7HuyO5l1L~m~CQ+?CMk7E^%G97Anu$m>U~Bvxbn2v}8LUkoIY7f^RUWbGaPpWVdfDWF|s@Q3ST9*M9gvV_4AP zAo}3lxZE3#cWQllFiXdT=K>s;#)Q!2f$D^Wccx{y_$2L~fDyq1p5rzWbQ(x+$$ zs3&$|WOGMdXqyJZtj}U3{m>*SA8Fn&M{gQM5!0uM8xj$d4A$?R2c8u0X;;S?ar$9R zb|=<#H8HN>NlYy*=JkFly6xxO^R4hKatC^zo0Ps-q3u-yLoE-RleHb#0g_B$w1;7c za^Zeymj-CI##(46V>gwa>|Ya<+kYgrAY*SQs2xdRjQB#*ZEVJ~EEYEnH*m@jO;?gu*mT_xHR5dr94^2!4qz*n>m{rCF7?`l-u9z!`m>j^lmW z?N21>UP9IEDllj0490^D=%C*(ln7t*H?@h648K1{nc6E(PLo_%x`@`m5RE6A8@%p9e@6+EX0d9R7n$i%cCS5kHLHaL2_y{W-$zZmWc{tT6x1pP!{3-MvVb+ zvO>$Be|zMIKqEp^ewnSpsBj3Ea* zF(m0SJ-3o}!NBwhAN#IoC555xI-mlP1M_zOv!m8e?$v%4uHsN32v&do)+)cyn{vDM zN(Bfb600!PQXP*bj(J21e1+R+E<>Fb>8^8yN#j((9$+w73N1d#M+w=HitM$s(vmvb z*vx3pA%&~E50D=m@y0W#&&GGmk~J0yl@C`LdY=t7O9=qJ0SkV8xIOg>wp9G+PQHEx+D5&z(54wI2)(m zh|sLw!!8aj*UzO4RPzAe5@BxED%xu;?a>0^zYsu(Ng?Rgyov2P7qgu)R{K0Hs1IUC3#MHk%uaQ~)xU)+y5!Y`V=*URG zIj8EDeBigM3Xz-ZV^5@*DGoSF-D|%)2M}cqG>k4^j zJ~7t~Q@Yh}fXdShQBNdDQUgKo2(WCpK9|?o5DppMRzMHfW@Q;96vFD+B#iGkqR$zu z-L7|aU}pnA8_w~cpPFW57*E*=GDNJW%b|~0o3@SpQ31-*?U8xvb$x8;^;&Kdx^OD ztuK=Z4EYG7i3_PfWWuq3VmaNrFIep^#a_agdGgEzYStNX*xu7p#(y?-MygzEM~O8J zw2tGw1OEi^_*5DY-6$Z1D%wImO%dZBp@!zFMd#ZDaWu4*6mdT#SxMtc3H2qDNY-^O zYbrpx{Pv)KA(IYn>X0WngC0Sd&)E7I;p!8ZIuB3SvvReA2MQH9+lkJ6G)Ea!Ql5+- z(S-E3Gx0@lpf=r3?jRiwuIw;=%7*ljQLTIb&a`lWe@IK{m{c*|nOoYF=nU&b|Ii2B z-ay#a?ryp%N<&Du`EcTWiTjlCmo$Y_e>5)xxr@}gSLCt%(lW$=6yr2a=V=wL4fz_R zr$_)^AX;;(x&o1J9Gxwnb-|mN+GD26%#b4*YkDQg+spt@=APj}_ICwmCx#v_LroPT zrglBg&l+O)x)yvI&i|i)`O!scQcbbBi+)Gv%PeBbcWP-{4u*l*!K7Cx=Ad>f(`^}b6qYbrG zGYnI*Jgry3#OaAtC1=zRsdnVu`2j?~*k`)^;GM)Tw*@nmlSia! zdK_ z5xZ}z;OZ+7?5Di}CNXt`xl#3q&WVQ&2PZmrLw~7RTY{bxMF8{JZqaO3+MT_-Qt?*$ zeNzM-zh-*ax8vP22&MbP7oylW7>eD_w^Z@c_0@}?#G!XoL-T`60z=)Hw9i9OR$>Nd z>Vu$K31SJhe;3*EhQRwX z+6wzC*;3~oBQaz>OuXa<64x|`pcsc=Zek?VS9kS;Oi`^|6D$7jy3T|I zQCTztCyv_vR63($GV`s){NN#eZ^xDDrTuXNOq9pLQ4Chj$7ZGBllDdn4r7X^X1xN7 zDXX$HC?d$^hB)L&qp09#RweW^+_P89tD?en_y^hIJY1+noCk1Lk;>z!Se};$!GD^Xko6dR1m>c&K;Qe(#Bn z=S?x#_J*U-tJ3qK&VAmg3d#0tUiSg%iMm-GlB92GQBY)&^3WNv2}w!&vmz~V0-13p z+j7OfR@!d(O3B+eF+Z?_(t<#`4beTVofS=i#mJRN+n7!mN)IcH`l$aGggJK;S|*+s zG0edE?xME~PiE0S%S(zXF_P#bkfJJ=IYA=}TTGQ$kmpuy2*SH=a;zyC5eF`*gzFt$ zazh919D(PM<2u-)pNOoe@3l?aDw~(c z8^baTLhVt9#9=i5^Oegwk?VOX6VqX0k-@Qaoz-Pe5a|mp+nl?T&V&0KE9b$*=C;0R zcje71LWrQ{{4g!!{flr`SwFL~AphU)0RTPw@;A&x39HTwO}_f6_J068r?={Axn>l& zmq5OF8m}iGe=ws1Q59nad3#F7esx|Km;&TB_BA>^TZd#yRj{oN@v^WKehT<+ioQ5GGzxfFQingOTlC<%la5Jpe60(!T{# zI>QyLo?@4SY1wAE-gq=^*ONCEm;g{(4R6G6P)#0T^`$o4{Kh-es=}c5YbB+3!8IG; zvJXUk?{Ul@JWsdtFR9_zllUg7h>Rs*Nt;d06s(O+rby%my0D__r%MqkG5qBiYzBO& zv%GwSsaX7l^2Wm-@m)D?FYjay{+9u2? zaXltK1b9SO&u={OW&dM)$;9)7|FBZXb}fz3o}ElA>SHMs zf*q9+E)C?zch}iLqvZ|8lC?&Uz%-PVl0v9~QyqV+h7r)sj)E@0{XpJ7cImM3gi?MT z1e%DogajXtWFFONr>10ej6_*S53E!5;PqDhLi8B;cQejhd6P%AM5^l3Nhxx4vlR7*te7pxiq<8PU!hS0taLkV^or$e4Sk+t`iAVNs_#&H7jh zR6k`x=zcF$Ry~)ZgR}Ncz+6XgXCn*4uIk%w4)IMj8>W0k&B~ZYwNk1|&#K_d1x`KD z+YA6pqw`fa=+s9(xXvCOK$++A4?O^6LgL+H-%2VT7pe-?ytdi_5)!MtWtN#@?*`#! z|50M-(2n^6VKw;;wZ}TS&qWKzNWdWL{VID29gLu2m>pwA=rxV<=qjOO&*w1~#!B_| z+H0(20Gl$^lW!#ui6l@!`GP5o|Cb{Lo)Q%Vbid#3CsSJA*HcM)w1=Rzu1H zBm=_5^og^qQi71XDZCxO@ymJ?a%5>d3o#yG#|jib7o!W2N)r@T<N+B#D7QrO z^i{|e+`RVxk+#}Bgxzy4aJ-bJ)!t9<9Eu?63S78ofTd*@gqbAB(DP6*_~qE?n^Xis z;&(w^`}khm!JBY$7SIP+mNHxH15viN(By+T%fTMwxcZ-| zb?dw@3ZM`ON*iR!otr|D@ z$^!pOy*W}`gEa6~;z|;9>*oQLUw_E!RM7A60HHr?R_OwM)aCGx*3(al)?>T&h9q#4 zfc}G3{11nF#zT92_FdDJ!JbS^!WBw>|`Hy(9c!ru>C(jeQft znc64GBkARv9?16=Nb_1!d1^+0`TCVWwTjtNK*IvET8o+zNUc%tw1jC_5;YF@-sk=nVWqiOkfF1sDT{)0O-95ZE;7C2E+PHH(s`q8U!9IgeBV3zo zGP0xuG_DVEFGtV=f2C7jir(I=DaO6p(!{2hZ$q2#l$qk&g|apz>#P5F+G8dGtl~{T zhkZ_kbMnIswP&R2y~KMr-R9SB0`&!y7hb)V`KsU`23O&T^Np!ep8+Z(jc`JFut~X^ zhJ7}wqX4dk1&TGHYkPpe!@HT#Ws^OaL~Q8jhi$L=A19(%)X5sT4mly<~z4-?GszIuk9L+E`U#-J83KGY^a zJ`ydf&D;Q8v#R6%KHNc_%Og2G_D|v1BtWgPzqy3INJtY(o14OEMxz4E-AbbwKlf8W z$}f(*!*KMx7PI$M$G8qT0B2lX3f9FKj z;6BHjF*tH_ZG31q@e7{2`l%VCOy`~YUT2qY1CEfDGiJv1YOa; zr);*l)y6G})jC;MLGOxB96A+I@qIT0+S)?(gVMy`EH}xb8fHV^okAxz209J z)FdKnw~E{Rit47LZ@&75uQxJbmv!^_=oXtdPfUIB`b0GVi7&9mrAu=QV$Gfat zJ!v@&{j0EQ>43gZP+i{rdGjE3{_)-aw)t8asm0npoes*>VYe~bYbqJpL~d5{@gngl z+{J)YvGBwVLhoew2eGK9x30qN@qFqhIDWfAt?Q#K*srruMA80=)bzR@mUvuoj-Ym< zIklW>D#!(@q~PVjeFi>9kG_XsA6KSGaoxpUEB<#>@+1&Yen^PqtNVOK*0riEC2DOo zF;QlpwA+C3V3mp2#qVX@CCx1rAmR~#5^ zWWhFu0B%niQ&e?oeSNd6io}JA;>`ngcoHIFKr)t(wf+cdN=4eKvp3X2`jVar3Bb5$ zj6;ugU+0kDYAl;ujxF8&dwf0Bud(ZAMh~R4bm6ZvaJsNofgi_|ym^Ntb=7TdQ|@`n zQzbaa_~hfq0OP!bwbZ(A9c#;Y^NoIRAzzpTmC|E?TC)71`Q}M01jmaNi~_GqopZU0 z;YtrLra3!6`~dJ;npv4!2;i|9-Mz|7+^rsXAxR74!I?NWtUy zJ@&X?qd;gZ!CKIur<8XXdp2PPgjlaVmNT6c|7VdeeQ+kTq#lzy;x zOdFMu^#ZT&!e&gho#WTxq%gK1aaup^etl4!@K#l@(U=Sb~JbS|E7^AXP7N{VS zTH@J&u99GpkP)T^&wQNxL>eQQjzCE8vj>+9 z$KlcKZPV~M{RE!7a22ECZu3Ppo540pBuvaIXXbnV%uMg5Ww`P*YU=nGol(E7y0Xl7aI;;BXBjHBp zL-?fKUP#V>QI(OY2C6@pVl5VA&HM-KNzyJCdJBP@>iLRoHyvv&GAdk8Cnp1mfwYIZ zhtDYPY{5H9&H8PCti~5%3@g~Dl#MXsK{Zw|HmpJQyyrQDhd5?C>QA<)HxGujIIWol3nzIblrKI8c#d_CBJ>#%JT@~0$qEt?V-exfZKun$-*B~lu(9r2wGKWj^ zcaPRpl9#w48bpyo430ZLsUBqNns>FbS;xU8 z;-B+mRzT8qR1iW~alBZh_C)hm`1Ftq<=VJ;j9>_Bgo91#`bkZ?0g8A%<)i)t4oGU!bkCxpp`345o^8&bnPEbf z5AX?2Py|pp{C(=FyGgG7B4kRq6&t$k=_?_)58}Z7bs=7q`|an)CnWn%oFRY?UZ86U zms~!c$tZ^UNba&D03Bja^Kd;1RxcXrhi6NnT)96>Q_IP&yhWbuPOBv64lNrs-83ujoh`1%h;TZ#>DT|FrAD`EZ6(5Y@�}MC~(SDwkQRg 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%0Du^>U_Tp<2|bbpBOZk+ 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 +# 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 0000000000000000000000000000000000000000..70a668aa314b238a2997704e02c3e175f998c657 GIT binary patch literal 2879 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..23e604a6a4d3b5097abb4181ba3f93f0fd56a025 GIT binary patch literal 583156 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

sPF*b|9iaq0_Q+0;NI?jjSpxX z28nrtJ%O8skKw-%RueOb4@rZ`2=ZJCo3fP3qb{V8XcK8)=soDy8DYjICWSeKd7I^9 zEn|ISm$7$oaGYk&Aufg6%00-#^HRL6d>}uEznK3_VChb8J0m0uD}*bB&qW5&K+%3N zP#hAE7oU-!CE1eck_%FtG)Fp3+9^ZIGGt?AzstVKo$>+lt@0NNsiHcYDIx>dT% zdZ1pXuhdW0|E7OzARGLKcEbY03B$iet}$jDXk2N$VEo*jk(Or~Zdz-)Z2D@JoAb@X z%)gi~nm=14mTb!)%W}(E%UdhQ8nJd*f3_aB{%ynC9JU(U1lwlYHQQIa*dDX@vCp?3 zvOjj99Y#lqV}xU+w4`bxoz%p_Xzh2 z_X+pYbac8dJwJUw`n>dg>9;)qkH{18G#vp4A-%Hmy>f`!+ zzLf6=-%8&x-$OsdFZD1gpY>rg}+DG5ofo#qkm*pWJ{zo z@+=CEN}_>ib#z#Cesou~EBZPEouSBxWYlJi$XJ-MC*w-Sn@mimA~Vt*#5Fu~e&){1 zOPMdT5LuEee^zDI;H;mrwq%{ndK?4AI59`8FxD$JIkqNtBz80QAx?~I;u-O}_=x!Y z_>TDb_|pV9!A;l`1&Q{=gv83kfy5t)|7D}IrP;phvh04@)3euQAI-j%{UL{tqs$5A zROJlHnU%94=XlQToR7K0Ty<_Zw>o!F?#$c`xyN#E=YGh;=PC1oc@=s6^QPsk%{!9! zSKhz*=zK}OCqJ3rkv}1SdH&w~uKec(-~x7mr65tzSTLgC=Yq`zrwZ;Cd@RHlDhmCD zrGVN5wh@c8d)P2r8)(qJ*ThG}7JO z-QC^Y-Q9hk&3ngv-y0v^aX)j$IKQ#R+H=kM&$&12aqa!=t;k!Iw-#^x-=@8-eLKRv zmYao}gZl~hC+=U|rrh4#iQE<3{X8^0yLe9X+~wipk>SzfapsBSDd1`2nSHnE-H~@! z-@SY%_D=1c#XG-usqd=a4ZdIfe)sz`@9)0negFNv?t91g;qPL2tnX!BW9a!MTr&9}j)J`0>d{!H=>Z z^*%a%4Evb%vHs)mr`4Z!eLD5&=BGEG#6PKgGW+E9DehCzr?yYic$8-U=X0O$f9Cxx z^;z??^=JRj$)C$VcYmH2+9bp(bW!NBkbuw+Asrz*p&+3&p(>$1p+(^>!fe8qg`Wxw z3jY+=6}A@+7ETkc67CgV6xl4oDsoBWv50_(jEIhitw^9qib%Ogx5&IGqv%0V4$%jq zyrNQ~8lqOBzM}D>#iDJZlVa<{n8nVB-4c5vCMKpVW-R6^7Ack^Rwp(jP9wfu{HXX9 z@u%W~;xgje;x^*`;z{DA;vM2sU)FtL{&MEa%`dONh<;K0V)(`ROW2ppFV$cAzASv* z^!4D^^Iz|OefRb2SJkh7zq)^o`kM2#{_Eg3@@?xk)^8WSJ^aS|P2$_{Z)V>-zD0k_ z{Z{{NNP;4a-N6)JrKLDLJV>Qnph5Qi)QoBpmVPYF zFD)glE^RLDDIF!9BV8vwAiel~)Axhl&wju2{q1+r@4vnqe0TUB^gZQ!>G$^U6Ebu% zJ7tc`T#DD0V1LDA6kIP-0W!P`a!1MoCynR!LjQQpr;(QYllZQmIpE zQkhnHhccTohw@$JH_F1wvdY@Zmdc*Wk;<9MmCBvUlPYUewyUtJoL9M{@>)em<)@04 ziiL{1O1Mh8O1VnA%9!dZ)vcuOKc_|+uTRMd>r9Ml5T64VOR>ec$y=6^H%-t+s!@5{d*{(kpc?6<=2KfkSi zd;gC9o%Or&cjxa(bz1f9>a6PL)NiT3RR5&@U0q$>RNX~ASUp+2NWDpYNPSUbqXx6a zNsa$B9%;PS5ZCyn@khf(!&@UtBTJ)Fqf=u-bG7C+%|n`JHE(J@*A�(p1$n(sa}e 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

$Hsx%f80=det8(;^=2-ghisfSmt~O* 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�zw(fc%%ORGU7_-l6Npd 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:zLJBcs`M|ai{&~WeW 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

+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 0000000000000000000000000000000000000000..8363bb65fd3f51619f859f0371ca3af6eba00147 GIT binary patch literal 125222 zcmbrkWmH?w7d9FQ?pE9aMT?X`aSiT8T8dK$rMSC8kWwT#6fIg>pb*??DGE062+*g`zMG znv$X(xGpM&?f?MMuYW(Z)MrF=0JOi;stR)YKC^rCD_V2UDI(u0^-dBPVTC(&dObFX z9kc4bdaJcM?K&p?{cQeMsg=2E($oL{S49~=iX8w?c$r$H;=dX}8W@a@dWIzGDk=RV z#HN5n{}C)QQhrpY*#9G?{dith9{cfMHRTkwRV*BtR3NIdnco%@UWeCC(FsiF$uS8(u7cu(hXVhztAIzKb{u9itYrQm1F4 zi;orgN5YtmOPP8>&BAXHP%b32IJi!SB6iO5dbG|Hyp0u-W>2N2kBj*C<(8mR8Z~=d zB7WKaRfdc>Ee#rqI6N}(7IDFB8=Y+%;vgV7EGZm>jUroo66@`w3@-w?eFj3SSE?VX z8vpTQ@o6^Px=e12q$wW_v2paq#bKUk;@>dOXS||y*PsKarWvP@`KJ^NjDkHqd`Cc3 zTZm(#)Q154GdOQBavqzk3tB7(W@Hs9jfFqS zvrt}J`;7)>WLjGhu+gU%Ab9u(1m=<7KLl-RH3*Vute`>)D9%e~o+u&1rV{4Ngd}s# z#FV|6%vY4Wgq{z1NxSvxU15&Qzh#rk#~R8qyszqpbm4o--Lj6D{hXB1TGb#b5< z<7AXR{%70eFnZk{!t`U&OaV0%-;zr8Of06dL15YVtIKNtbVkOtV}VAGSghVHv(IwJ z#Hb#f#hgMW(w7Bq!Hp>dy-3!EZ2|BD|Cr)>Zygi!_?lJv)tc)Rov5UAJAP!3yz{cl zRnpO?qt~70sJCDr6KFEy3#f30#r+H^AfGf~FRo9EIZBOdjT8fC%1C;S3D=Ig+Tnr+dL~ zzdR*YGibio|5HJ!nfIgNL*eb$p06XnZRM@$*#lEFv~&;_47Sp>T*Qg0y(D5EHJ-`# z9Lh}gDt82Ab41wpvb+8m{nh=AQr+s0B_V1y0N@C>!iXpCS?@13D)y`*7dA2NK`=sG zspnAPf{En=yc=z-w>LZ6NO|S>XP#E?k7_o)%5G0>r~mo_r1N)k%OgIfx@MelEK%^AhCMqBg&`v)7wMHvD7AT8Oh!ZQTAJt(k#QiAJQ}Wv}g6V(Vfz1_FTx(XP zSn6>p!fH2q2PHyEJJ`bKAjGZhg-smXruqPF?9R`Wp6PthsH+e|*;}}c$Z}2ToCGiU zA97H^#O$)ONypJuw-BoJvj{^~Y_2joag%Ca8nSK(B_l;Vk~=UlzmR5+I6ejosP{69 zt69VruH-aQ&2|14rvM?Ob65x6Inf0rk!nnA_qr!wuGqvSAtfx3TlDx|reL+;o~Uf| zsXZ0*P?X@uyLBV2ilLp+s9s57rMIKMx=byWR{tlOWCs9v)AQv;zSoQ`WCbHYxt-7{aNSF|`@#SNWV?n3@PBihV&fPzJ z0{|KM%E!z^swB_Zbo;}`%Gpfe9s=M;!?|*nEp`Q>s%ZMRF#1gfB#N<1Cd~lJ^s6h= z)ivF6AxSll3C}{M-f-MlT8f08yg|K>Tj>9j69Dw%J6c!a5ThmIW7E%lpEB{MNI?}h ziHtRwM~R(`bwo2(=uiPAb;u=1nF+LO0ABzW{Zb}``?*zoQzlIC(`p-kRaj2N#*N2B zZjSyvwBvtbGyS&D7jm%sqTj3VI<+}>ep{>oOVw0)r>s6wK9_mMKDCLV`h>MROkj*f;Sf5?IZDkz#7C?C_CSQ+}emEWfob1K#qbFMSW>s@o6 z>ImxT+k4VmowM{IZg1qH)qgVrcoKhDxbWs|>ou7;eX>#f6mEZlmnb}k$a8@W1`!o$ zh6pv7Yd?^5tsK3RWaJ3q*NAwVp`eq}jxARuDuYP8_m%tKI3XK}!o{QSgR}xL0to9H1fDGouib7r-L1>silFF8@W$&RYR}f z-o$WB1HHt&yLu`G8HLsBw`3|I*#Yf$tHo16(|IvJCti(A6<_{cUp%9MEF?>u-u_#q z0D!92_HUsgsR59pCfc$2w4BEoII$Z`eYfx)0h;dJ+rxn1*{?rEb1ec^2Hwu64MBO} zXmqj1g|hf5;5S?^^HX#=WfaYC-GA{6i?NefLqEz06B@?KtXW?Cq>ck+1;Ye-2$azl zERMhX1l$y9yZNntGBeVcY3G-2x@7z`?ta*GmK}66zvbelZ7d7sX8M>>s39QI#0_G2 z1vz=Gz@o15BvDlu(-^(7!a1WTWnpPH5{B^~j*23IO$l5=;@e_R;9(wLHbZzs3(%9eR@e=)$dXQZ)U=RUON#}&Q)$~f ze~5M8GzYLnL|k`M@C^-(@3pvltgN!#cwA#Wefoc=13ZPk(6fw2vMR+Rw+N#=UlxLr zDZsa5TTveEDno1hpq#BReKcEj6$!jCN;r1yX}RKi9yG(3{hF08*r1Z_AQrcGoAhx# z>|9`K&CjJRA`!w%nG~Peq}@bb=mi!=^>CCTNMn7n|8rpr;-9B#Cjaswz%&^mjZ2f2 zg_w?SH-wjE|GkPjgnLAEh_I>QDfA-2QX(NtBM;l{F<4nvt=9-0ksq72uNFw}uP!=3 z1{Rl8T0!DO{M6amWh$~T&pj9Ay0^L?tj>6BUDjAapfBlYm+pr(EGh5xKcF; zs@H$CWc|$welYjlx9a}o1w%fU&ePPG$sF}T3f^y*tGYdFb@0-ks(M}T+wJAt4Yrz| zj@S2#z0bNfIcue#Dh;eZKVq6WI0;ah9ef1NdfC=2$~S9 z(1t8zB_Q8q{b=M}bHgGV>rN3UrcqKj&`xI>(-DFmF!c>TKB-RSp)5?~R8g0uG(y)c zWF|I5M?~wO`NneD#Jktf5#shql9&arY0}2T>pmA!k-C4ovqPw3qb*bCjR-jN*0FCf z|7E3aFY&@(`(uEIf9ty4`+`riSfq(-n|bwP1^}D?$q*2WUsV919xPd%*lNa z_*O3>%Pg3~vv@aeAqjqy%l+q^{!H3?!kc?)sJTgljqN`KTI9=+ZRPXjoJ!d}e0C?# z>3YWkqRU*<5OW_SNmNXWmMj{Dr$zJVliDiDzpGXm((7O20=Y9`8h+7%b2INpyvKL~ zkL6;{#B-{j%g(Osek3#Vb&SM{RN|zL*JY;oJiPe)IvhALN%J(XBdV1F&F=WR>~o*` zc`oJW**_|Em408pRu=2MIH#b?>Q{j55SEF2uT{)6ep9rX1P;Aq!%Ld8QBYW1;>xH+ zB@@Ue-0u2HcA0AAq!uLsqsLl>=jaPkXI%+0woO()6FCE~jy-*uD*2O-`NxlS5{7U~ z+VXQ4w+Q_lU%d%@*q9Omi&ysguLSApI4VtlMY+8JF(^J~|zCY=w zZ{ZfG>vd3ZdiJnU{7XPO9Hsw|r;{vQhBa=tME9+3w>oQecX8&)+iTNKB|pg$yTj^U z#__Vmy+{5uQIePDB0I5V(?iQ-$2&1(C0_plRc5HiSRaL!#)N3-{?K==k=mppN)6!e z@JqX45y1OZQWm(*W*65kTan0RhZmRF!mmX5KB@}f{kcDz~ogd*EFcgxOAQcoHGURQJ(%-4e64@(wQb^s@d8LypX(yA zWZ-Qz%ZjaZe2F4uD~^uVv-lj*i1hCgfpzJ7J`rVkzHEQ4&0fgZR@-ZCiQd~1fEf=& zkU##F&&*71+dHpSnxE#<01qQwn8_4vyuXW>e^<>-JKl~DR3V{+VYH|!@VQ!g8HO&+ zI;_e&@mLaWr$?J;m-~_-Fe<0%0OcN3=WMHZU*oU|1s%o zcfsVlj>hHs$##W^CQ*I+Mtg>656^O-o?)7XNBKIJq~3{GR%nqzHfMA0pBQidgz%Lj z(z$4SN^W@GpR=tOaBoBWyRN{Qm=!I2=4D+uEteiLYi+SJ9M&22G2+eyszC;bt(C&w z$(`6^5=W!mb*Y~2^mVK1`&WI+po`i&g?D6*A8eu-E61d%Szcdy8GXauYag;%@!YSO zf}~VA$;Q@fdsc70AuVq>Fp@G_!!;V6LF|3s)Vatv=C`8F%*f>#(EgrU4C(n}#FM7* zH+XdMbId|+dx~@(iIduEI`o+f#c|;T+e+Rtd14~^TS=$X_CcFKlL6tOYD+o|DmH*3 zyS^_tc?fiSE};u_t|P9EuXX?FntpNS|J11Lm%u=5(Reg{+0R2m+uISLAXOUGXYZ_~ zHf-K?wr)3dWb_XhU0<4?t~Yxhq%N;hXEbJ-gfBL^ED93+D+6^b`r7G=7sLAvG2D1- z{O!qrp06pt=Mw8v>fdmtj4>L8QvARsr20H*m!gi(>x~x5X%N@1=}<;X#`Y;Xof^Fd zACCgfn4e!s;5bwvPT+xjz&R*4RfE0M)_wUi==SY~#^qj{&Hjwwf_>2KoZzuHJqHI) z-4ONldG|oW2_Ds6vi8VN9jHU^YKh6!hjb1~QU@qxYK_1#xg>C%X2osK&(IoO;Uoyj z!|VpxlYik$bMS%>mnG^}S2(RJX-@(ZvX}~l#R8F{FnSRfo*U#&UI_6MBZMdY6;!;< z$Vmk1n@1sMRl^MZQ{Xj_)$;R%vUX;=BsFd74z{o`Z)f!h8aJ??+f)3yuJMnw=wq?t zOncW$)s$C8ii&j=?;V-S$dastrG)aboDUzub)@D+71_9$i()-44N`&xXM}v-N=-oeg$Ini#6Kw^`14 z=rLjhZPL%*f1C7B=q(7?y5YOMo}wG|&41jX8yh|Gy1>Ukr1&32<^45Bd&69M6N^9} z{(^6%9_8G7uFzu4Xtwu~hX`UA^w@8&Y;qp6eV14w4SD-)Ir?#=(oBA?HV@bIdPur1 z%B`7w8$XA?lJy*S>!JV8ZgvN2Z@lugj%B6U98DR+`g#W@wZL*nQJdijSnM!0QXk;w;yI zvnx!-RTe0?(rKCtUK9@isN168LUPQ@zv44~RYv1$wuasB)b9;A3j;dATvVpVQQCGg zTe_YS)CFb+3SSu~+=3rZoiX|Wwsz$0ye4Nj%)*7pA}2YO6BMv_W~)Bjg>}~&S|r|m zP5$swjvMebzL(6uE*xRU`h55m;SRO|f#u1ymx;Elm?^8QX_@WupC=#CqX2J+?-&Bq zZSjvNmsw2%@s>T=%2tJ<0#p8U0q>_C_v^k?)asDHTh**|eNHDGCte)gC!EP+G3s5G z@ETXt?e{r6=xOFG&e1JT#I-AU4b!4(v7E-i!)L? z1aWdLZ}7&KoaYV7N^UQDNaEyvIkqD}^$7yH$sOx`Z^s5WZlXU34eR8cGB}byZi^T^ zCO_k!H`kbRRq1;ml||N;v!k&LEWdJjcJ=D524QBNBB7Y{=SceGkF;P}A%lLP`kyP8?RA)$rTylW{qseJ19aeL3fgKQZdu7+C5R z7P=pvP!^SYxHT2)ay{eZTSZfpt2#;Ho;DZGCIdGOMr3Id>tgiz0RXbx(;NwD;@ute zv|&I0fel~SZZN&&1|t(d6|$L5*P$o}`U>U^w+UXNF7K<3c@Ze*s$#(TsGp-LAw^NZc1V#XkQJjc4+&5^UjC6Ss0TXlQuheRK2c*%@$FRZ_(jwrE zm#ehML!j=H{MRn2&8EKZIxy_UwD^EickzyKuR=6G$iUvx=T#A3ZGeM;L$K^djD;mS znVahK>udglr*)zmUw2NKJ@xhlD_H_neegM-2xD;~hJP$`jkL@8b?kRtpc~MC^cBTz zJ!{;KDr?wU-*67*aQr$zE^0%klh(ERo2V?gla%w*J?=1CTwrg(U-fuf)sCtWj(bLq zf(jMq#tVix?Z@x-^1EyvV<;7esS{DTUV)2hbE!dL3wsu z0?WxA1nnQnddEi;ZpV;{@dKd3KO28qjTx@_uQ2t0uw`MNe4d&jGL8<(PrO8o1O{nG zL9-1)VX~iE+a`;hTzKQ+g~p15uTkL22QScV z6*7OEX{uly7Lj|YO=4#RCK|r!F8=AsB?Qg6bUIkW>blePtLsp06BN*%cZEveOrsx) zGMcNc5F?QJ3`kYz&j+7bOhk_Eg zAQ^8LplOEPGjB$o{!|&W~W}!{PqUl|TZLhzLG)$vYv&jpJZo#jBZkyg@K{hRN zG(CYT9^YkEeh|VH4QSWIHm$RM6JK%sRpsXHpjy|rg5W{8ZE^s|uXMGq_k0pn35W0L z8VoxNm)2g_Fs7~z;X>b(K8wX&hUvRzk!J5uKJA_wk&^o&8%J>%8)&-_>e?>)lyLfd zzJ*Zb8?MQiw786`DVDCar2GnT{GgvD$rKvW1bg}HR#v{~l&rL9Sn=YmF*;~1#lxQ& z+Aq9MS+#pvMAnN-bF?iEDCg>FJi!Q0$>}tH(=8436pT+E{vCThA!z$D-ZgOtJ5~Ik zOz+N=N(49nf!rioQ2S)mYvDDcFZ-FCDJKw4bIpIFM_feI`tP7uQM^Xi-Hl@HrLcDB z4wwu?Q>d-fvAH`4Fd8}6c^_1$Qx|b4 z3^U31$AC-%+#X;;#9k(FVtyAnFPkni#ohOmfDeqNH|d*lNDeQ)v~%}7-eh;}wBmjD zjV7vg!=T$+88oK6XNASChFRsQeb5+kx0-T|$pKj3`*$@oKMvGrj+f8RYRvn6;0HC@ zxaCdM`<)n*zAtO^PM@dfe-$SEAMmBnL8)+pj6_4=tFh0nxqq}IK)PeqXlAO4uQ9wJ zD!Q43&sAN8NidlGMxd)tzRn)!8h?j)%XC$p7AugiMI!j^W>O^tZO8zd$@Io2xdqx^2nU(~AcwHtP(UN{P?>)1KS%Q)= zEJRNzS4H7hN5U6)5ec$L|1cMC*6UAAmWNwuOZDIx5u zB6Y#;c{m^r=6SYDhg$m{m9Mut9&2zQU-2NbmyJE(?RZb%*SRRcwAVef#g+Aj!vedr zkB$E=RE3{MD!kqen5{H$ilbB2QWv@e#SPC0=q~<5g1I zHf0YRz9c7lTi*S`!?D|a`)&b3TpTKG(q4PX)`QgW1V)KrQTV`nf^Ku(UhU_RYbU=v z_WC^PB%2XN7aezZEU2aJ`7iloDvL|V)8~$I7XQV=D8oSh84oG+Z9COj~ zv*dM(0t)ggxaC7W2##8kr0!=dmWU{v1L7w5e%ohl6ZwGocenV;UW@K|N-nKKhUCx6cIxr9K|wvAJaVuV#LtP7gIM=~|MvT=frTnv6Y)*!OVI<} zb>EdcL%zA~WljHa?LB$=j;-)MYkHuYqx-i#NnhLNuvmZL{V?+J6*QN5BdW1kDX;c% zP~SQH!r|EFf6L6PUaUTfmw%)t&!ZN+Wq#GR{_>02k35*8D)`0ZnVk$i9h>MQCjJX+ zjU6r!^fdCCA~e+2)k`3p$xEtW+{w#-CjisGq=S{4!oLEUu}ds_F|VUJWYu@(tC^Mm zyn)lbkJKs{|BHKO`?X_EL}19EdonmsD{` z8{P4v(H05#!c4t+T5IxH@Z$zR*#4IQy!GwA=D(oV%<*nAx1roGZ2GgE6*rTX{HzR` z@57|HikN%~IH~{O1UrA?@}6&v?@<1WacN8w!5JkCi*iIlyWDvS@}JNPzx@{t4j-vC zahje*B`Nn6YZcJUe(8y%A7Y@>ePF}(FItPdn{L@#u+^TQdeZA!=4B;8$onH?wTfUm zW9XeA^<4S8khjxRbnqH3d#2Y5iz=_qZJ#Ix%DghJ8(tN4V%T!j2yR|gQ8u{zPNPk2 znQ<7N&?(^hjz{o^`GHg^;@QlP2q-M&<^!CPS@VXXND)bC>q&~x6u;*}`mlPfQmm&8 z_O~)VMBZXleH(e(RnTrs4=K{pfgN@}aeIaP(vl$y9$ZHv5aNv`|2_Wg!tll(b9} z#`j!W69hs1WzPcL2pxK77WsX|qj>Ql5Q<-|vqVp9KDDzu<8p6u`rOfpBlsN}KmnQj z0iKnVZkWj>NC!51IlfIbQKTjR+MjJsS;c*a6?#Jfb=i)_L|$=w6(+zx=)w1slAXNq z5DkM|NSY@8kA7Fi7r4v9%n?mew0O=xCx7@fXVzHqiz{jnwG8`Y1;V7=x`xKG9hAFpTy-gwLcL~B7 zUNPGQZFwb9gn%oXF%JZbO1BiW%LXwUjD{0RLuOw)NMQP|J2t>ep2ffeohaAe3*oyd zZ9e5!_q&piIUrxYP5IR;rT#%C9tRfy&}t#i8sdZ@liMDKVpRaL#aEA>3WRpbX;1mx z!Q?}c^qw`K+X9@K4a$jDN5?_aKBpKKsJs%a)hCU0-PcOHy#L#@D=c6@OOBm#?5rt5 zh!xDf2Bygg8&sDg4o>(tg)9t%lniw*e+!#W4qGoAhbM#itiY?36KCD{cJsE#hE*KH zp;QMZ(9p~WxU_5u>pC5cadp-nx7Rxu%J|2n%!iG7aD2qmXMR`2(I}k&6jVq+|13qT zgv3U$s3VrOFINySn`cVF1h1e}l6?30c?MDXa}oz=xC*^J%WeFW0o>&AdxN z?gwfYsegr_dL%;;vD)&vn>g1s;NjG^7+&j2%UfUgjB#}h)mUsFOryVca0Wulh5VG; zk7oW7?aQxIDw!0aPMKD-_aO8R4#3xL5fuM`omkq5&~nuuyk9btZ8KAFX$H(*CNCy2 zj70Fbv)x)=KbxTh)wHRRo@AFf3hO9zVvvk z9i$ETr+>gpTUfx;EChE1YUmUh!tHwKB?k*MsB%qP>5AgK(D_?j=xdkJ1F42b(nn%#+ z3tt0sc^7`0R`+^hTz{^=$Ob`Y{_g?+@*_!4OlQ0d5a|WaX1uL+dlpQPXxr+85f1qM zLlG(O`PPe>Ss|bNV&Tp=INRSXdrz4cxR7XB*xQwsO9PEDZLSocCD#A%cv3TIak0;4 zE>Rq3;fq<6g4UYgUThMiprxmf*I;5W11L+71)A>Bf+NNp4(2HYgS^*s2T`u-{lwqc z3&kJD|98B=nN-J?r2Ts6NWWAQD><0E3(N`RgLtVMD8U@Gl#zQ)a2sC8!@NE%Xn?1< z+853sOgv`Z9iN6e-_U}pns%GjeVay;bz|L`@<-#!}(LrRmQCxllf3c5cEE<8@C;w?^@+%=ot~ zKm~?@a%%t!A1M_8F!3A`#7PY8HSIG?YaQ&X{RnR;XoBB=z?nHn8?ROSSXYZ$bo@TR zzl%|UH|2zj(#EcEYz?Z()opLAUpn=Fi7nlfYyLK!gY14{ty)m|5lP2ZCM=Z{?&x;gnFQzGP$5*7N;y%f`zRO>oQu1dgd=IX*on}9_ zTWLvb2-6r0A5)Jr`Yo8T2s z_)AGypk6QsO$B5&Z?UQZQXtdil-b5wj5x|%7q|CAt_*WTX8lHgJR{Ic}XOwiH-b&FI@dtQblY+ z?|NO7b-m^>(ER4`l&Y9RyW`n5Z{B+VDZK+>vq5uHVHqsMv zF^$0p6ScvJY`8IaeG_t_nqZQe?U81vb^0woo^?G!0T#d-1CM*9ppu0xM$cQE`5|qr zyf1REzpD`EdRstA1s_*IB?QOt&-Z=SA~WT^Iv7gb|DvEvGwXV(8z-@~fiRTUNK25M zx0I&73I0j}*~1A@qX2CPDI?v7uMt_*Z0=l|;*k0l9A8bpdEHN-x!9aHYBijghUuC! zse!&8gQVbJ==fNVHzQWkqo}a@=P@o=<^kOLXIC{k(eE*}xcALa?;m*+HfG>|;rAmv z0c!4Ltv6ZB{PEb0wgKdi;a)cw=N}y2fAKnTe~2%0_6nEKu4cT@8HC(?9i7fZc6B63ochTs;4WX}~!HNG92PUDnH5UDyQH$}b4ErbC#*S`(+? zIqD8br?deD6<5-eb39h)N2nr_u_?EXM#4y~=IJxUTXkC#*qvu8>DY)Zerd|y{nFjV zWpP#)YjH==Lqwp%AI9Kw&Av*{U4hk%s@T7S3VBu@C!}L$61g2$pU?h?EY0q!;J^cc zB5HZT#|VBKP)zj`|wI;iN5ppIl%K1KcQyJ zD+`zCW&s!blnaI1FW(w#H5B^q$WD@?1b^#yjP_P_`FTp3sr zRi2W4P+%FIfWeh{fvNXGbsQ9>$MgFoa}kmT@pgC! zm%xqpamM}6e7;4bqBboA9HJmYE!(cwCKoZ7mGJn>0$!}B(oEJ>Ta>c#*QPUD7T>90 z$)me1AMSgVePM)^J9BGTV?<9{d{b%0gm8Y;7SSL0%$iID@$&F0ONVsv&@CUXbYFkA zle=O`%}lMRcU%f(eR1VO*xW2V?tVLb^I72c2)^{?KBhW2O7Z{5T~LMzdZx-4wJua9 z+z>OBG_EikU=PZ}@1sEw;)tn-!v!^Pa5dd+jJv{6Lh4|i9^6o!@462mQBj;7XvCwhinM29>DY!6g;H|t55;m%XwaFMv7%c-El zza%VEgsk~z2?J$wMSbxCtz>cI&=lMjqT=c6mRa&axJL;Xzb9+`zkhXpj{#lBQT4+z zsCQkfl(MrndvGC1nu3@vPHHefb`m4CO{kii-yVMT-2JmMX=P`A8WhqsRpSm|cXuLp_JYCFE6=|C#$1uVzdt?foqS?tw9NII>F`Qnv+94QPgD`AV}?HIvpW4^lBI1u z-O8;P4j-k35nQ5!fCM;V|G(Q9EcbZH)<6beT$__H|nZX!jo;H{AsK}Ug( z?TwY@!D+Lrey)>G6_pD{2#BvmxCb^|A6E)x02?Rx9|JH!t}mF2oF{QFNyrYh;gt20 z`!BC2=KvE@fdVq+#c-wDv+r^BYbCKB=zDQMytuK!NON*}a?EW%Ulsr%kHilC<4lg;p@)cSgFC z?|(J8Uwt}c5I_03`Ko2Ho+h)q>X$})K9J~iPi#UnaM{g-|6p9Od3^ZNBO|N^e0_iI z>!f|lgTJs&nb0B4zYzV{lS>I(Z$4ro#Aqdn>kV$Wb24#?J1N^qL+bu^#!;7NjQvPg7gi6RN8MRckiTPXS?lk+TE`5r9 z-w7+C_%jnY8iV*n+x+z8$t#^Yt%Yk(lEYukw{t+*ma{>gy7#IW3OX?%(NK=uQy->T zwfGzFNs%wnUy8q^@3>=pnh>u4!X!EWbr9$G2yt!xh|TQCF-?5hP?ckjV~;_MAB@2$ zp>D;@_S$Yj-3rhD`WhQ>7vz4!vnN-T+3>Tgq;#a-co#I=K0p3#vb;i8j4YQ`CC2OQ zyrQU_KEL+1WuRZrB!)8p!E|sL6dX6Dm(p<|-@%PKF*o2uot!c4BdIP+;W_i@g#5(v zFM9n8V)vIMh&Ll}M_D8W2aYWiNhHi%#3ZjGC|{2Y`kV-QDl`LHKjmXClCiybP67_2 z*nrdcN1%bEipZ!)#zN>_8pT2}07d*PYB-w$^r3}~_9M~*I@r_}B+6mq>O^eW_v;fv zqWKgv7&fUn+RZ=FWPEoBvMs%&8-g0+k}ay%;=ESL$f^Tf6#YuXgI0GxOG{tfC(i2Y zeIrZOTY1U{$IY7*-XyEmC zInz4NL*tPj+h(s`MuR7?Ba>amaG}fhIN;9EP1nZ7$3ku%?&{nq^7e-XyB9u(?P6wR z$AWz@-(7-M2Ls6=V^Vi12o5^Lk}X-L7Ad_&1g0lP=yHB2eu_nr!tHvHnT zuk^xG5{mo#!t=)M==Y)fPy!|>0!h#BvT>(ziS02<X&P+*|ZItGw^O!V$e56yN@s?|L1RdXe==953izxtzY6 zk)nUl zE!hb{5w~{_rU?S)EK?a9W_dWU+J(0`woHJ!G&Tlf_>?H#?e=s~r2Hs0t*`8ltzo)B ziN(XR@Tp-zS+zN&JoTc#Y3|j|6uLDp)9(!X&e@hUtIAm8*@r>%s^8cmAp;NBac&U5 zd8UUms_)z1^MZd|$2U#*a;*uYLvBn*a5FXgF~R^tlXdnTjhmIdnF!Y)!6>|Una^?z zY6dQV#0O^?fKgeMp2dYhTUIN!mmBU|e*L)OVy~}e#G~|bFYIk&{*MiHZNVfbK^`{$ z8Ssx3PE$cBJpGb+9TP}$;&46l946`hi9~mZ4d;51i+NoPNa9A?Toejd$6{V5B!ta} z2n&HWX&HO$ch!_WcdW290< zRG%w50knXR`Gu-|ly>MGgl=N5(+NU@>8e^Jc=Ex$8}p|IrDR#6)9g#eOB@Z0$rj>) zJI-I;!nNQzYp{q=kZshdP(HTCm6$CoO&Uyjn{`t$ah0AFgx7etH~uEy`sAd6cslM(_9$7yfpVn*!hg+l`28aS*p5N$b_t&YLP(<^fT(O3g}f+jM+*y) zoJ6AGG@&ezp09{9QFbB;*%MecEu`HU4@WHc6Uhm;hbJ+l(vf+c-QzZc1QB7Vqs!&F zv23XkLKU*S>#9)o%;V|uL|ev)D~OHL0OM2rN5LLpAXaE?Oba0_ zW#L71@9r?j=e#ASE78i_qoi@4e;HU4{Z3i5={&Zw@2*>iR&c>I1w})S8#h(XC|I*^ z&yV)0^b$p%1p@PLJ_*4HzKj_nH-&g>b)|4@#RYnIl-Aqn-Euc2v&S=t!4Lbiwc=c^ zn44|Gvbu2*pXt}`y6E4|6@f~#?^oQ%yGv_yi1^&}N?av(D&IMsfpFZj+;NFK7FNWz zb`8F7zy5B##NfL&ba0f&x+C+wWAuJcY}kLa;GoR8`p3{Wldh?Fue_l3(Sx5K`L`be z&pZcQW6uG0b7ywDanF+5eTbxxT)(XMOd?r`e_jPI1E0n_ZnSFMANb|&_Db%x4*6V% z;Fj9#5KmWl?EPN{vn0`FtSbwJ+xwH8*m?XB0jKvah8!BS}?1v-zyUB=&zGMp; zKt{?K05aZ$>!(Te%hT4-qGF5Oo6L!G%Ecj6*QO&9K z+F=2;dH&|Vsj6lgc|48W&F6$KtK9)dI#aCq<8^GAz3=cqmDZ;vLvn zSdfR^RD+0*{Otj6mKz^AWS&5Rl6R?|U;nNg@!B%?aJ`((ayESUJLhaneEQAZdvl~# zmDXqZ^IG!m6A{k9QODe+YY*n9Yne?;;u%|->7{|`Z5C$3&K244|GRK~h=LQJF|Svk zJgAuw){4%{h_bGckz5-M@su2pzKB<^*YI#q ze2i&cwd5H(MMP64-pDtZ%@pr7teb+CFBPcsO|rvrakfSxYsZi#(I4ytpndmcWBSC( zjow>5bjUlR{Bk1kf!IiRylSWE!4IWSOIL4y2jEYHAzU`ie{m`{%G2$;f1D3|_1v;z zdd3Cv&gzrNRJl|cF#B+R;@O6Q01=a6OFd9h3B4z9=|QU@M1U>?-rmcwZ8Na~WZcxx zf!mUcMF}K`GYQKs9A3#RXGgP2%1-!_PPHs4t1gs0h;lQ?_oy%DEk&1Wpg*=mpb=_vNUkRVj_)=AX7ZtuCIl`r0)}aD zd&okAr4ZGir}6Yar?DdP5cUjV0H(st^+@pbvw>=>y6u7T;4h~U%L|?2q0)z*B;DIc zt%ed4AOoS+ws(F}FL@`Plq=6WD_T<((YigLgf;$)vMGRYXR3$Ol)So{_<5YJ@HSyBp;=&X}C_r7}3VDH_5d)B2BNY>8zj-(ofX9vv+;cP1ubvK%4@H` z=B-7yx9a?vX7}K{HIKb|r@6@?bD#F-WPiV?p9m%#kU<{yyMu92305zia9}eXhAs>8+A)o8P)wQpdojp-0`(eyN&3NRJ^)J4jx2LkqC&_qxfh zeXm=h8T?eVo=^7z2A_Bv!`9-GC9X>6-P*k zyi24{AsYt@)(&0e=);}C{)*1AoL#vidtWOikBQ+Yqm2ftS@Z1aj(c}CSoby*hAt=V z?bHV*oxzA~>hq_FLcl)`6$XTKV(BfWZ}(36A9z;0>A!&kN&f0m@PJm_h$WAz=r0?r zL$V;EjGfa=lVyW$#PxKSFRGlHNye~Q@0_x=-!EO0f7yPK&ot~iGZ=9K>2V+F$ZxUV zU06sUrvICJJzBF+${5sWjJWG}TJ*(VgH3?0!|!i3M;9AyYwQ&d78>}OfBWV(%{Qf- z(*!pDG(OLo*J*_F!C^)2VpO9{R0{9@(Q;Gm-S9C<6TT*DQaEq^WJxd9lp5SF_P%e=&nHC> zt*GvQO<=ixVDt2{*=gR>L#pi5S9F~`c0Ax|%j@K@7gx2iDwr@P_cv}Ch+vJNl1EZn zS#Qdx=n)#r5&<_&)Cs{O1h9+lWq(Gpvle@BpM#2A-~2C|x=k#( zzK_#3Au8r^`E?1?vpa>@;RM?n`+szXPhEA>=E5e}-{}4nNjxL&b{r%^Z~cgwSwEU@ zs&XSkj}gQ5Ve`w=Dk9LCvtA|gKtXLOrPTI_BgZ(#yM42PGTfa@uCGk}8w07Q8)Iwj zCd_d=UT4|XzhfbcZo6glv}t<|m(tbcE@d=Kx4KqGtw;LW^{K%H;PXs&<5!&lDBs{+ zUCQXB9iF|#ywKEQy2_agE7>@eV@B)`WoX)mkKGU3tA=Npl?k7R1(S1!GxF7ZIku$| zpJuup?sIG}0k%7ry?VH*&K=x=RIL?w?@xl zYrLDRbr>8VN5g~(Hh&DfI{NmtSRi&?{DJ@kl9z|UG{}@r!Gd`7^f)*KkxRR+N9D?j zlS=YtiWlsAy?}s|?YG0VnYB!DcjkUyDw;BDn|_{ON^B##faGV!s2mg&b|Ghg`EhiM z&M4NdZ6xFaad-%cdi1Ph+UfdBx^4RNH;aVrc3bLu`X1OU&lV?ewv1R>V)W%Gz#uw$MN1Udg%3 z3Qo#>^0g&a)f8r1qo3v&X={aylfs%Mqi<@v#$Ha}wOa5IB(YldUF$rMcgc@rTP(%i z8x^Rx+gNi9-6fCymOMD;P-%5jF25elovc*pRvLDRZv0k7Yr)N@oN;k&{zZ9O2|j6^ zA*TCi{*aXhc;>9~fT=DQb_U;VWc{rpkyF2*rKwi9YtGM)5@9(htJoT0Kr*<*0`(% zwnRUNl!hlGDrEz2jQMa91O;%6!{JOni7fyA`->K=1*P7|-3;q{KY|1|zLD}_p|X5x826GL6|>3|$qL+@`_I)r) zc*O$sC3jQ**b@cUzWu2-ZbS#17v+e~d$k<4dV22E%>N{K97EgnW_7*jpX=Vh?!RNo zp)y3^qrNBPXK$7JkUF+tE)GSP3xWx24}rx(r={c;O}mLvK-bBixPwcBpbLUaK}ucj z(P{bazje<+vjSSMg=gT8tl+lz>G6byGp!}kNcgzn>u?J^l{S_WK}w1tMJW5mvh(9i zLr4nyx_In*-9z|$`7q{SK76LUyqu6RAV^KGe@LUM>y3wfXS_%KyQf?iZ^oE|^LAeT zV&ngwYNS*4O#w+7s;}obdVF)S*%{MIE?MXDPwVo@+9?88?5Rst9N&cszVGlK0HD|A zKftzLI>Dr%Bxwe<%LgLObU+3x*nxmqVedui&Ov&EPu8%#&DD4Fjlz_Et(Zu}NWfuzgZvRJ*wGyfI(3%8Bq&D#2Cr0k+Nx`y0s@fI?fP?_gm zlb=CVBh^5lzlBzo$UI8gM)E*eQ|N{KOWPouPP~59fxoh>+;`ur9sPIj2O63(MEUQ| z4{BN>DLQ4WAoBQsv)gF2KswaX)x`DiQB&go`99qf7OlvlvwXdok-$VfGJ?5kioKkA zrn;N<^?v+1#NkIA`BN!k^78LzH!XXarN11T1ded8v%^s@yYD?H5R9y0JloXL{i?SH ziEZj>gg0*kQoU>K0@Sk-A_)j$2@BtBrBDq8`|;jtW%(_Ykqd%_z(T>Uj}d=|_zPLO zKaA*>b8%iIr`RdPryg0BRo{K-)r-BjEKz21r>oXbNbQ{dYo%KCGHd9Y>_-*ksq2Sc zmER&62HRh`_nxZn{<$pB(lDLu_zojzQDU`(L19p=^#@9F)a&1yfd|}_-Nz@BW&CGd zlr6-vCbl&dd>XtJo5~F@3pPIs`LZWTxfoV=k^Y{)kt?!{^LT;K@m;H#i|70v6SLR% zCw$JCWOwKAJa9(xc&DU9I!@C0;fu)euy5^B+NpGQER`yFhY3s3hyT+rUwqAyAdiuVWcXig z-(M~52>F5>PX#t?3!u9_TvSrYz1n4MHVOr^*Iahn1$>_ zq43c(bbw4Zs8vVWcoLpNP$+AZI>J01imGW;eQ;M(rVx za`Y~|-&9tZ)P_EC$zE8Rn)AZ>useeCdOoFZ84uCUuy%e^LtME$fV^&J2?WDPkNcu_R*FsfJ>Yzj4-Bq zDesuMZH;lL7|F>lf}{$W8|^*OQ6KX>fL=Vlp|~BPi^(Dk%bOlY4>uXZWlH{_aLk5$ z8%B7oo#=;Z%=MrktER~I_~t0#N4Vm9-*CSZpLbQln{4>@FzIT%f9a_Y=8DYYK%Htw zVHG_rtgpZ&u%UloE$QgYOfwcQNFuQBn2TQqn)XdI6cJqXM11S~Q*#12;J8LlHn51k zX5jB>Tsjx9T2v!x#t)Wy>k2YSD`W&bEdap!}^DRv(USnZ>j?c|}4Qy;|Kp`RB^lXO# z2a5R6_)y1q85^UoacTm{2kL+6NVVz7Qddz#q00s}U<0XC;W&1rY&Ng?tY$&@xLAaQ zNZOwqL<21LKIJ2jRh1jGWyLS3^%yj}>JU+gszABZsJzj?sv_z)U zj7U+jF8Jt+A2C!?c4`_L-Hi=PioUkh6Gyt%#DB?Y|{ zb(({bFuj1yZ#PWouq!Jmp$FkhYyph@{G_I%>^3F`VEB~GH-`AdoZ4!TxL$=C1!W&3 zm;c4xuVah9?0`D6r0`Ys|06vXD-d(WqD6wQl{O@5RP5*PU|!dJJHrL8-?$NMck6c5 z0W)W396WYHPifj8u%FJjavgFLWlmuD+rE-NzOfeYvQ~;lwy-O+>vx%$SgrP4R+(Mv z1z`L+ogC#b%#~u-NDJ4q)p~w=dRv}8gQ%i>noYw3q{EkRniGDV9+(E*O^7|;7cEnn zzd8(XcuAPGtY}KW4#+TX_6`x!897g1?TLEA3$8S8jsCVFL6K0A_eK$zHGew`PQe78 zgI$_C>|Q>ltl0dIGy-^TbpJyi z^+>fwmIo9tLUB+79ponm~GG)!h==eqK* z;G#}0;483<6NS)XXzSO#Rozc%jZa?D7_vC5m_n&DB1(b?H%r%Ee;=5cZX)NapvRpt zWDQW^u_-GFZ%9#EAKpaJHtexQn`Dx*8X$?CkE1($O&mI+)a95bI?eFV7TOY`!3c73 z?R4P%3vI*SsIhCdVAzKIkig@gbkGBL)TE!672B3bQN5i))Bqh(d1=1|C|t5zB`c5vm_5XOjjx#`h19~%Kwrs zYW3oll(EAb7C!RIVJb(xp(2AO_0KN~-I0DgHS+9(uK4}Xq*6nL+wU76rkodo*ae3h zD?1K9FiaMiCLTt6BqBpJOSgGVgQlG3zufBz58Tih;RlT+`(7OrDto@4ug0WMDyGoY zJ93KEL@6JviT_bYe^rs6F&jvUetg`e>?0GX-4wmQGEw2I)Y`RHi!Q@KmmF7e8h7Ln zbNu7_k5AJsexCLROJ}x@ODDEvvnPf;ix)gk-rq<*arAxS?s-r1jJ+Y(h>d*G9Zg^Rm?-@ocFohjiIZRvEfsReu|BNXJkwJir!uyla?||aUf!Tmm1=t zUEy=;getGNa^)e$g53hl^;n)XG0y)(y(i|_u{B>4bK}(~1C*e?fuz2xpd*_qGl?R} zl>FqKB^ibq;GMq8&jEhDs$?F$w43Bj1G(YzeV^{{SRYp_RB56{+cYa%)rB9af5dH_|hafw-`InbX3n_Awqn)UcAbU z$Hj`_K!x4%@oZqGeT!eb{!aUkWiO!GX%J9t{<;+@TV>g6QFU?N{^n+>-STGz0on)} z-6xl`5U@N8#_S|L9!dR=-5g+L?QAsyu2&e)eRH`#&C1S>Gqt)p9X_-_SxhM#by402 zksnO*4v2tl_}ytS34XlydO$}{40U`UVy=||)^Y(kwLvcurUej@LL&Z>GIcN_G^*IJ zHst4C%pi>qZ|F6f)WB)$7yDKy6ZFXkB% zp`Z}@5D9ZMrS^PB-FfJPfV?**^PFo9-*>$^aequX`I)nQ^3#`p<<~gP$>FW+>R`<1 z%1PE^cc*{g&+Yj~Vv~;#3~Q7RO#2^^`1@Cc&4aq&G%7y&v%yLd=6_!6D8GorD#%e+ z)ecd7RzoKO20_)-2)JIAz{*V;bTQc3*=w}>KI~8FXuJ=PQ(4VZVh-s(F&!Ue{hBFk zR9NP)P{wIcX58FK)%<79_lZg&2K>zoo7$nb_Cx{vtWa0>)wkKeWyH4|Z zS)1nsLG?k8TKAuV)C82;1OGExlyvZmDTGkEoZXYFEw=nqw=$b+lPXn3U4)$Of;xmT z5n#9+-E%4!*xPT!zqTJbx_0t8a0M>h{_w8X{f~`Bz`^Y4@*ltLGn>hF(`WGDuh_(uRyd8BE{F0S$veZy$;PO+3R@ z5*(V_6=vdZhFKwyfmabn1*S>or(Wcx^eF2_OckOm7trR0%}&bnxzh8!N{3U(v@NKB zrhC1xj0U~mH|Qc};PFmbW8gyYhXX}Z_BSj&{Nn$$hLynX11Zi(1Ih4H5#HyP34R|z z9!{UF%DLCQdey{dYo)YEc;f&)$-UanhTRBH^|g&>NmYUJu6|901XmTlbj{N#y(2N! zr<+UfW?GxM>YwR%T`hjEviEdy)Y`#W!Etk{QR_RogYlLA9#2eXE8x`s5}Pgc>8^lB zHH#<(ld}0^cY&6ZfOWTAhc~WyHWQyLTlR|Ba0Mr`Qnx#m&!<6_^``7MPW71y0$Q2y*?AlTNWszZ+(Ey3V(&K5(S?jObi>ww*5xh1 zACjafF9rxu1Pl4_ci}QF<1)^3O$>Kj?*~2S{!}{%``PgmJLphvWrW4U>7M9?=nduz zVPBlXG}(}P-Yj|d)VK(QeQ99l$|}N+>YT^#p;TLeCrb^Y$pv(WfN@Mo%I;32qn(#2 ze5V5l+L2G#K)3~`KGio2V}gW*bp6l;=gaiGpG+UWM6Hx4brwg4k1uO#8=GfQuG>)=7F3H1 z!+Oi@5+Dh$ql7F*MN9hrDlwO-vwJD$g)SKL@X279AV&weO*Y>`=5tw2ARD0E-Z_j` zK!`5?GlLxkb$OLrzYOzgU8nmUyI-uZ(Z`Pcb<8**t-rYU099n~JvN~m7fU=jLdYvA zB6;H>Ze6qbyTny2EVD6Szc3@NgcbjpyOk_@w7M&6rgi%zavTJ zv*>w03^j4F2#X4V>|OtL7En%OSOUC|rpsYF;K$+la+m9k@PeUMr~4)DpNdDcyCka< zwnw7Ck}`G4kituDyrf?RZzY5)ES?bWCChi=Y>s_N3GR1-*X6&NKEJz$Ch{~T9aBB@g5Q3r+l>qJRkb#R9FYiT7t+OITmG@7=nj8b zZJ(4W>B%NnF)CwIx!6MfPqrH5uX`8|*X8?fE|k$ZuV1yj&Teo)FZum#WZTg`qC4gB zhF~L1D#YCkWtvt)cXdfxxO!7s8#DGn zEs7wM69V+`U%x)-3fm8KQH+xZD|J-I@3`Nsc#^+V_K}*hY628pe>ocyrizRP>wDB` zu+kE9k}5Hp$!<9D{DeXRo;JlXx|hkKNmM4}AJTA|6UF0>SGiinb~_go+o$n*lSb+3 zP;GLu{Z4+L*Xu1SI(!R#hj-|nf{aRiFZv@pdwW9=tObD}iK&(PxlgjvGvb5ayGk>U zBSMNR%V$L-#hym(G0 zp|FJ)*s4Xr0wgZ%&m1CrDsupn&K_P(EDj49hpejK*kx*E$_yLd441&_+EziQ=?!@< z2+OTBMXZ8|@Pcp+jeiIvP|f^M-ozW030`vg!aS--PR!TUgzj3Ujl$IY|6&r?SGhfU zD)DiLp{oqBv$ctcXk4Fc;h{$vwkFt_?Os6bC0R|ZwV~Ns0pFvHUQU*q^zfhpeMZrH zVm-sRPlt}#zX<6DOs|GV@umU`pc^LaJbC~_zRwD6s+!tRT)dXp8PsM#D1Wb zVmw?a5;EiWaR!64J3x$Bt3BpyjQI|WJBN>DAAEw_Vb{0&+*XLbS}r#wnrNdd0RnMY zO-0RC%*-mYuu^175Y<}CRMgG+=`9N=CUJvd!_4<%Iz)pSH7gshyhs!AAX*F}PQf zGOZwsm(%{BjFE~F4?2~Qe~3PPdnp8OYwVnNq# zaieijDU`F{dp5g4DA73^US)MN75>L#Un_#++BX>&qaoeOdf;%;=R&a)j1YG_5O27{ zToJ5^8DwWs91wB1rnhM=er<9)vLo z;&~UDFk@OWazq&o@lzq9;@ywQA-V=N`1Ro`$)z^wZw4R~Tkf9~{w@hT>X4++G~r^{ z5I(*t%GN!I)fdOuow+@v7iQ2!kHMi2mG zsj#8<^}JN@vkoZN_Fo3`ekW*oyg@4a=3+*WR>Vg5-BiWTaCjT??(I-x^%u#&fT|yR zm&>6YzV~@-*(XZlrwPjpv$R~mh=b8XT_ek{YK5Ini|an8jLT0Y-c41Hv)xg$KH)`T zJqMs@3`x73LU+SL#-JGa=q>(}H;>z0Dlr>T>=9T*tONjB_U}fc``})2+01)Gt$Ueivs-);}D=-gEIFhgnHQujA>8-Txn5!&S1oF z#W5CN?%mAiZ?YhQ51+Rb@`0h%H#4R)nEmqTsu@qY*)cw2Z*6b_s)MLs>m&syL4;$9 z1#FDKFQm*tON|jWye7arhz=1z8t124%Br6Yh3?dI@L6rlK4Pn`%SSU66?fwpm{79= z0gru!YPi@ovXFPdV}Ke`Tmg<(g^c;MvSw}Z=UXy-{K<;Y2P!V0k-hPKzixz3ac?0s zCcWp)cEuvxiHBj$=W^@ReQTKJ#t3(lOP`AT#S;qYQMYy7HEQL_7G2~kI!wAv zBGh`K+ZFRUGO>Q1(G#Cz7!;<`3*X-|a7X5mEgf@Wrtp|?UC*5*I*kVwL_`lILm(0( zEwp)k34K1wismhDsdumh`1JdwAc$PDEMPQs*w5%{kYu+PqTbJdU4SUi9iat6B<7_= zlW&sW=W8s2p5X>QA4Bq7E6SyXLn2i;0h}x`iB#zJdCY&KxhQ);HKDM`08A0a&&27; zpF0@-!cIUO<1%b3#hz-4F^yz?aem=h28@apozARIoNy9lbXDVLQPokwwLD$Af1?D$ z-bb@ySxY%;%8BDC_CSwD9y2HNdc*ttpt`2nn_BN3lmg>WGw%q8OlY%m+xoW8030!% zC4sIkIcg9TZqr$E-5j@?+m1Qn9x{K}op}K|kZB=t`mC>zfNni@yeJCs$P6A% zr_ch|bSyk>qN}NJzO*)`B}XW|CZ|U4KLn+xji&age@a^WVo;89(KIW{g>;kYfK4Bf zI18$pVx$1lm~)vKl)z?b2$d_Ufk5dys-S}1%EB*+-j)sq1CM` zwDz45O(LAvq+xTg2osmInuSdVE)}Nu^PG|C#fMy&Y3)bphNujv7?T@gT*|(XTd!hq zA2Y#FZ;6Nl2c4$*vo>k`nz)BTl+kisrTnnimM|nk8kNHHNjuqpzbj9>c7icp0Dw7!v-|lyb|2T6y<8Ngk4U^l4Ft$tZlON&g|Mz_U~W+2u0hAZZ$}J zR#t4?1+<{ac@5Q4HKXMgyTX^>EzvP-{)sZUre+}c!(4PKe1HQR3f0$XdF-Tmk~hl( zEJqfc{F9PUk$XRw1Uy3^6_u%Zd%Sm9<3oOfiVEb<-li%3Yg@BHw)V3^5Gd0XZ#PB6=cD0399YB^{0i zWY*#T5`R7LT&PD1CFhm?OvdiXpG+6)8TeXrsQ$H+xIc;bLaYnLI7Hq+_v`Fawv9=9 zKK&``i1EQH~yDI4=no`8lZD-vJT3jc*DA&rSUoc-O$eL-(Y$Pj0lSRqNjmf zQ@a3G=gsJH6O!Mk11W5FN{p}JgHqX(;v?W?8ERjXCNlAT-_|y3CbVUQ!$6+8Mmem3 zxp)^0Qnc;njp2pW)zQIzR+DxlZxW8&(*2AiI}HAMtZIG8yGF;O7?!)4X9Ww-y&sR} zozbxKg^O(hW5;*qFBO62!b)KY$0(lg)EU42QI3Py$uSIWHNU zV$1&~=Hzx}TE*r(kMH_C4rXaGeJ?vv?&26HU+gw?AZSljGtKQac<%7(q|uR#c5{aa z;q1q1SIatls9bZ9KG3K7WNx7s(kTGdISc*aaCVqSwNsWsLMNIyc-GM=mNslpk{w+0vG^5WhIzJ{`@r|5|OBy_5)VzB>3Ul2)KttV@L`q+9fS z7V9qj&S-RN9Hlyc$}lN$38}>+UpJXLzE}hm(#0jQ|8@lX-=0fS*pA})VMQY$z{y)JuFY*c5b!(&n(y$HM58N);%5aJ#?aL)X)o#V=E z3t3c2uhiX5`Sm4fk>=0DVt{m_DN*1<*hqWCeSUc2SrCTqRt$&VpV8|-fq-Q0Sr7k! z_cvOvwaEq5b-1jvi_>X22R`T)y`4n`LRdr?0%ZScNcRTV1^)0lPLBP;FO@vk%IUGH zUgnF;3*boea{;d(&5aIN&>+1ec$zRWmSTLj4zV10re0md`g^^L2>?I9t&l)A9^$Xm zFA}HlpERPJ;e$?vu&U!!c?EMZY@e#-q|)gMg|yr1c-|x%)F`sZhs*O)7^oAEDrC0T zSibJafh&o9E_ChmDw9h2ow#WkBsE2Dq4&T!Hszsx$h&-u%?1#u?Cz|Ml)W0s`5wU$ z`9oIeAD7LZuTg8tbD@&1x$Sb_=3?I0aRuiEa~ra7m^G5=;5CxVceXNv)h%7WJ{hMN zhsLvOg7vic!=8FEspaxIVYuUv@F!^4xIOD?c5nAw&a_F$sa`$~ssBEIFJ;`8ZJi1% zs0^HUzH@qvjrX-6ei}moE{QW#22lsdBOm@K^tHqu1>!SnYibC*SM@Kg*TJz5H;PB_ z_Rp~U6$1-^&}vkpS7GUMv~6c8$wYQ%na+&^tBiE-aP>Izw|jGDL~`+9L}4RQAq_t( z48Dl$wx7K`!YT1z+1F*_mqB*iH(=k}lj!UdjqQlDrgKX1q49xyvgwT8N!oB*t6R2VrvbLS1&dt#oet*Xl%3Bw$EPBGnhOh0P<^S zlgn{Ivm^&vqwGe;lR7*=k2QtzE9hG zkCrwAt~^KN^=tvgNB_9g`r?&aT&CAhYML!JF3rZ6Tpyj!4NJCu`O$zGm!%rVpY!ZP z3;T!Un({2`$VBWOYHI4-yu9Gei6U)Qetzvays~raDASbb!II^rFb75v5s}WN>5|@Q zK|w+Em1;|9%qzL{@@a#z*PWNijydCojAc$#cAwE;!WQ8}!DWvJPq%ETEJxOs*hgW=R_=u`No|}GE2Zr1@1b(ZF zG;23gA$!2O4;a^g_&=6D?zP`Klkd0Q^$cq_#a5Q}I)Hw~J!sQo%-sc!UdYy)eG00s zJ4Z_8lj#DX90#$B0nL<8D~%HL+JF(h@dG^LKVOejb zoCM8&cH<)(Is7=ke@GU;&6h$*OicW+#$Ord&nU6&qr5$u9d_C1vWnr1B`P^PildIb zhYgEfn?P+Ip-u5V-vWw?KDoKMS$j_7Yuj=^!)G3%fdXYMFvs&|L$#FEtb%*F0YjhJ?a%ae>W8ew=9I&!`al<6D%yBKHT| z-i-nI)K~fChso6P#y@GoK@gLAN4jxTBK2qu+g^!`bq$x+_|dJ@@3oxPTUYWceFF&c zF(c2ZG9~_Sr6Lv^8++Wa2LF&QlM2u=$$wE7(yyMvM=m8QCRS^vqJmqgdO$%*IW{rT zRTIs7HFHmew@ZT+CB0>e!q^(@hM9If((p#UM+;_s*fk+>M;y-6n{gn9$Y@VVg$J}j zslj^+LH}V)4d#MPUjIe2^l<_g*u?PNNX+bLngICs2Yoz=W*~IFd^C0TkDcYX7>L|u z6a5NM_9MO#Yh$bR$1o5l3_zx##t!xMl|+X!<8hvfd!K=dRI|lhF)AH>s4?EMYnq)N zH{|b~ymg$ZLPsr%5KT?%t=wIe83R+(=#mmHh`*X~Nsz`seI6JLLHkxwVG~&SUMttU zJwSSEG&@cy>d7i9X@%DDV6pa8@u?K5dBx>jw~D;`@p)Sr>T z2n!7*U*fZ9%g`H+VVVRQC0<@$zHafV=Ev**Qfb;04|iYhiNVUS%>VR*0H{ES+8UC2 z)u!_La!y97*5+B(FrQK*I-GF4VI{7pGB40k4Ojpc2!-F06+UpIC|GklVfYRm#0RE*hT%P0W`E9B>@RE1uhqFte~= zXcbYS{c(6%h8~i={1ANj=vq%Qs-(15n)M!noW~`pGBq)YDl6lOeJ(Bz!TU4PeEg%V zxbw=tBH%~%NTBCx)CkF~``}LI^#R60t8a=`Vp39WK|wbGAwC7aMca^jSl6ek1i%b2 z!wWH`MGZ7PS_5$w8a*l>Y~KtxHhh>Dy?^V;sgWvoSrbA=aoJ=&RLDL4~W7j!+b8_-8|b~!fMzGsLm z2{hPG@y+G;C!h<7KYn*_Uu%Jyp+0yB{A%|XmY&VK=bz>xY0R0W_WyAY%NY`J$p-LX z*3FEdHOP_aUQ{_PjCy%+n*mRH3^L-{H95o312}y$ym#BUjt+>6*Mz z4^acZ#iXaRn8+dcis@!CQ-LnukGYUl~(86!rKfj8nQ0!5(QuSlZopl$~ zd7YNn#{9$)xuLWRRCLfnW7nfzfh4%lmafw+N zFj1&kWwP^NL+~BoAD-RTg1+nU9Fh7xITgzMmYrAd5)jh3N&8XriVFOHmR96neY$stB45C7)WY(g;1d8JA2lH8*1ttWGWJZa zPmOH0==zkdjSnrKv zef~Vbnj6ne*j!=b2dx(8bx+VlYkGxPtz!^-Z=Egp#Y5Lp(|1vFMb~@6;!4rr*Zh}L zKDpYuWo$b%WWklE!QL0j*r&RR@&u>Bng>xm1-#XjH+WNuO6r=Ts+ivJ(}Ota8nc#A zklL_>fQP5;$B`kPd64YG_RyJ7Z-n9xRP{)X>=CYuEoP(4bE$ogxELYK^BTN;JiN+R4L+t94E zi6ne-KWkg!%mnp8dY_12LE$R(HL8pB{MbOg7G3AP)(F;{bi3Uv+njKKNe8 z<)k$~o_u=5$n~MZ21QB@zsg0^jW$KJ3r;a-YBfjnC%jz=jz8-ewx^MGPjg3ye|uE? zs?yx#*yA)?|N9jebvZ5t!M9u)N!jCV7}kirxuOpL_~Uw!w(vDjg3YS>OJI!apdVUU z(@zXC{aZ^Oes>9Q5?f;GosHtAm!F?s7s<>WNUHTEhd_~K_0H$dZDEG_Ifume z)h&N4xww3INO>4to@%c=7kk9qk~My8$bCH-So!#otL1uq8p zcT&v{$dp_bjYAH*w-kEjyV0$zX4GC#oxFhs9Tl8I-e((V@(?7#&`Frny!AV|qK})o zi5Q+e*${I*Np zsuAVv+>Ai0pfQUB`$So-VKLQ55 z?+W|(gCZF9HM}TACS@#iZ7^!VFe#vI@OX1@+WE{s^)TO4V{_A>o~8NE%Ln^|iq3y( zo_@j8t5D`{Nf^tru-n&UDY=+eRr7zV&f~UL^MU=$5pZR*>y`fcY*zGYHeeb0j)&Q* z`q?K95ArT?14Krhzx~1f+^Ns}bS=&3~iZdVt0-~je zf9O&{K}Pp@vWogpuMakc!%;tMB;dnU(+1{GA?9rXY+a?*0Rhi%nXkM(+*#Lm^&6VA zMnBl67B&c}zh_H5jt8YZE-GNq_?1hn&qbTY?F441wp=-L0=mFw|>3H4mc zn%6v4K8L7B*2Nd`3Qzf=oVha{M4@ffex%=x%}r8&L&)lrLV-3#~(^9%R)X9CWeK}Huu&x{WYObEDopS};$h`~ zaUnpmr5<i#ds%Af#=fGL6_8&!VZaSc_! z4TlwQfIxnHc{{Smuu)L-|9^iJ+91@mMtTnr#eiy0v2sl;blTqbP<|x_hS9vM_$Mt9 zfR2=?XRY3+%y>^PY^W7=gwtLQ53*M~jSI8C>Lx9CrSmJlDm~Mi@iQ9_A+_bW!o*{x zlqTMWKy3naO9Esipc?-;iRdU`15sj;+ziQf+}hlu+NFYZ!6Rz;sUoNds^bKj8pH)+ z^MF1jwvLMyL=jJM#+(ATX^7T)X_MW{PMib73WIKC-{9`2pJJlP;j5yKCf}RYAD$0r ziSQLT(H5Z0Sq);ojD{Krb@bUsIkDHsD66eKKHU67OhJ^_(wQG=(oHKQ*+Al;=Z|Rr zGmu0n8+7MvaML&5S+$M5L6XlRzTh}et0qC}xIvv(UDQpG?_h)jD36a%(*M6<*9CKN zGLOn~zpK8mN&KqTW!p5TpKCrbYoJ;*ZN4{j%bk)(&^n#Z z+`2sdHLCeAtp67dRs7w17tTZs-pPhU2gh5UvP-|S{iA9v6P0D`7sB6mPCov&J<`ir z2Vic>V!ilH=#kr+%N;lHkN@eUoT{lDQ zQ9LzSE@>^@X0Cs;rq|lo!h%FVH|bK!1c;KJ48~f5h+|H#uhC_bFGPmTmpLNut#%=^ zO{Z2{(?2CPhQau|(_TJE$^7XL;?X_k%Qo>Y;f&Wlnn<$umdiFMqsM5|F9{^`IrPr+GViq|?8=&z{hCCS|ASosY-Cl%kj zVp};czpc|zsW9lV`wqLeB)9s`qGj;J#r_JL-C3h6^^3B#AI{|j3BmReh7&P(4wsxb z7evh-4H_f7gOln_j9SQG=2#rc#5a*9UM+yt3e;RDtnpH@@l@|xCU%+ky7BVydZyLA9`=*HgBv;JXn*;F7)T-7065!#Q%DZ>=)I8JS87J@-yMgt0`@xdBW0oD`;|`mQ%4RPW9GgL)}Ro{VVczBqcb8y&Ju_(RL-cMEhmo}wHwYl-_>OBz_? z$!@O0G*ADiZEpFJF_>LPQI7m2yDhdNKg&1y5d3$HAuDnj7%bmb$g;syc?)=I8E%#R zBIfSI>}wg!!E%$do0b#5^2G02Uo0E{F14t{2_a&c{XS*xe|sTOlwLbqR1$D`$aTPk z2yR*B>?1Xf8gec5c=0h`$*?csnfB+}+UNgV??tp#Aju@y2x$1sYQT`9_hz#nJ+Jig($Ev`a8%f>!=fdQsn=1`jl!bp zAzK%_7}v_NEL07VoPG41N7YXi`CEdy;EX1>`lqFX zLHL3V**LVKRz&48B?$5fjsdhV#Iwg(Nlhd!A|{Y4TakB2sHRKihAnIBt>vJvNlQ%~ii-7s zX%c3m6YKO_Ca$GLrz(*Fm}Z&Hs}WA`&ZDT4Zl6lt%G|lvRlEK&Z7(P&U=|jpny$3i z-g#+cL^qNt**nU`#U%jkBfGn?KcG%L(C5bMZ`X4=H<13Sy7`Rk>@p1gBuYsEWa41Y z8p=sa3Xpxf7rjnC<2y(H367OdVH=CQ%)6^RcCu27PtJ07zDQ9s`P!a{534&jRMCsy zq4mEt#Tj$VI`!M)*qIqighvY|eJq(*UE^u~^Uj}bL)ut9(W~Uh%1@fw^ZN(nZ*U*W zDchytb`X3o@QaMEAgsmK!O6UR_YVg1DI1wC^Ts{_u({k&tuMxc8?OnVl^sZ&QtqpR z_GUA1goVeNp^#@WTw-hx)xnIABQU6?X?JE-mQ+Q#QdwRuuZDUCP(>iw!-t6n%oZU4 z;&02bfqsV&bX2<+({Jc28JIVJUVdy;W!vcLjq^G{#SsMp?!AZ@#mTa=vi?7&-a4$U z?uix-9y~~K3$8_qQy>KQ7I!Ib#oZye1ureG1qu|3wrFu$ptu)zDDHN{`@7%w+~@p3 z!jr(6z1Lnd`|MdWvyD!lE(|d6X^IKQy1T>ktZZz~zJ-M$a~jkG5l=1grlzJ6i1}wkR-x9&+4-CT2eDlIaL#3duaK@iHnA+u73JQdL9t-< z%EMo?XyxdNAqu-aiY#i+FGArO!I-eLTjjl)X)a{&^T&vRbAq7p9HoJ9B}_$N%uB<| zqN~UyJ>Lq{6T^<5{mFTBo^Hr$rCUh4G%KHGJ+Buf(dVQkW!j$ts7_5IrGq%$tSs}x z>bVh$+@FcmV(|vD@cSmDI^7y~AMJ53L^((Gj6t;xtIU??TK7ZWA zSXeNEf`Ss|B7Ca6mjg-fE)OSSl{kpGI5}Y*9UZPzx})uZcY>OYm6eqV>pt;GN%)Bm zcf#ASfQenMu-T~5TzW1BYOwiw6&`y%=B5tdQ08tiKL)3LV zhUobPUGbzTGPvY1DexN z2NyF(!h`i&lr4K4T;2g%B5!&SlU|6cCEZ1#w4;fqYj0G>`TFT;OGd!{fns24hLx0V@W zX#%gvzmMR2&I9z1vK{thLk^6N>Af4{Wqm3DmNH1b^FCIgqM(ioXRxMU4n{w{my|JC zd3|u949PX6KMm6{ziXDn1!pC;In2BiX4lr%HmaBF?n1=+419bkyTf2GSPPu{TH97z zGAJWk27Y6HAdgV`0Yz4wiJV@_}W-Ndo6Ou4OJP(tNEY>F!Z37b^#g=wPdS|w4BZg+S8*NBs;Qi>e% zZl~0i5j{P*qg1KA=@T$U*i>NcFF8M6nB(muHe!_{;G72M@Gk71M{X|OFo(DaqO+8C zv3Max%BuE$_yaOxXaCu-F8S`)2fJ%NGj_a$d*E{XJsBF(x0Cec*BiIJ7D>x|8|cjM z437yzK&LV2$y||e`!3Rv<Y)(8xSgZ*f?-s*LIoN6Ln$r)WGuBG&^#Qt&20inYVvn|-~UAXI8g8Q z3C$Kr9vFE|(UIjYZqj1V=fE1Gc>MVZ&y!|!=Ey1bwIu>)a=<~0y~_6%H}ybH9LhLt z+R?ru(N`4??~A^V^vIP+=%5U|R9jk|941FNJO(=_tQtelvUx!AGf0(K{%s~ui&Y7} zUHI>904D4J6_mn!a&g<2Ssm=rHu*Vh*|Ie6e!}MZX zc>QvO1Uha20Q+`K7V^UqZ~vBi*pwb^1iOnyvY(2cI239?^5;;`wnl79ZD&Fxiy6XPJ8epcD5F=)r|c?P_LsK zLBDgiw`P1*wQ5n%9%ek|?yOYI<$KY8Y|)eX{5wKZYUcKHK-S;s(!H1Mf7`s5uT@I- z|07!ju?{5SfVltxneTswFn!`-^}=kWFI=T1NnBkeTA`I72#5Cl6yiihvgiY|`w(uS zKH`3oABB!Y`wmdHk+m?a!^L0IWOWmLHEt_K;|9+4~(=i@VYL>ROwISH!{gSB4 zCqdi@$(*${E_U3HyTAUKpb!YMQ6VBl`ac}PJ|Pf|vNx51*}Nh5$ti;>* ze_IhhOw9nHg`#=8e-i25j?Y^%7=PT87sB`cZOD`7u3^8q{JQFS{i@b0U^}u`UqNtE6iS^JJESA_^hM zj7_jug9}=Nr(we=H1kpeB*dN_ACa7GDK_|#yg#;Ie$x>M;s#szTA|&3znplZ6>RN% zZ;YqH+(-hP+ofYnz{PyU>B($0#bc=Qxy3;)p#+crXOds#QdMw>r`_((?l`5hhbxq{ z>^*vXaY=EtkX+ZC@b`yhvAz~$upupU&;QpA$|t+mHS=21nnxu>SN34c^uD31gRi?Y zfoK@VUPlr@3JK)R^bYd&ciZZ{86In>P^AC?(}t4O7|4JwHh34qc&6N`D!ZuscHr%u z%#1Nbt1Yl}fsoMDiNrLXcI4!8<^6*i|r}CRFJHG{+fj8@9hhn!n zQx1I5(-NSREaf2+bm*Q}^kv&Kz!8A~FO0EXOw;d0nKFlqVmZx7ddB)YU$jHgsT*O_ zx1oiVR>H%i0cOlI^v@%$|&`48}q__o)zAZZ68li zo#Ho+Ljc&b;^J~L(;wAo@y>T|-2QCJ%s(;58FuEvDsJG$lK zp}o#C{LyJ@*${-6vHp~Z^0>L-%Ig!YqN45%1gLid3tWK~4)G40wu z?|SW_{qc@AI-4)uDLQ(9HK2us)MnH)|D7p$le^bZ*T!cySn?`Ca^w^LY|3rrA-H(n zJC9tM8yhUJq8miS^j|cZLj|oP&b*H$u#jJ$sly^Rt?KFJ*c=)h%nPDR5>PtRi z+K%JBi~y!Yc$CGPrvkFYWm2hJDKbePoGtZhQ1b=Aoh1a`_~nbnu17TjikD{Vl~$E-_H~R`lc^Q7F6$2fWQ~)Z>3I z>FfoLcPu4vv6C!!P-Lk_fe=<<%|Bg8RVb%9f99K>puWbyo1jC4hsa>2_K(!2{Go7a z0Cdmq-_H`$^2@{=arv4ri>$Coc13N|6DrT@NG1fXC`~D72pQWY1JPfZd2I*D2G53? zy?5wfB^wN_-yx^naZnKTT0r>9fuEYPvsrBzS^9GW_wQ`n0xAAPYIKJmwh!RO zIX;fe+&4q@!Rh*aY#V=yIVqs}Lt4S0KOF8Sp;T(Q@ByO07Ay{&^RP+i=Kz!so3s2g z>9+XrTPFfDv?vljFD!4VRKW^f06<^r5tnswPoWRs zFG)Wuuc4RR}OGR))ieBlM6JMS(Dt<@#P8#8_FXDU>)CBfDMppItj@(xn_x ztCAL3DB!o3yZe0k9tnB$isoJ#;b3dCuc!8TYGBt!k*`vj!V+E5$riAAi6Q=Lh$F$> z5>#~vW|T!vWhis~>-Cau?p}qw%0nQ4{W&&;45(GZ>dPrUMtWY}wLC0Jah)yx!|8WX zqj;-P?BNGg{A}y&C_Fh+IqKk$fG)cH$zhL*#(A0TkKG*eD@{pw%J;WIUiCvGWLzvT zEnJOfa45cf)QG25BXKMKzH{J9{CMu3>NOtv;B(8bmLn3EQn@)E8_5 z(>DH3(x(TJGD{t)oaapyYE@-cg1j>znK*B1xK??w_N(2ZN^G*xeQb=1^^l*qz;vqd zvZ+V+2UOu!y6kw|#{Ru1weq4)-tM&n1)&hN>3Gn4rh%uiFLQFALOYd4?>;{it-nE1 zxK&5uc}vmc{7~`$c^m)f2TS|ScGG!GEH_m4hm`%p;)cV0O3C@*CP#^%i9&gY2?cZ< z|I&Kd#73;$NNV}jQ~%#NNjvQf*L&I+2mx1r$eV*+zA)9_q(%YdANouO&Dgz>hVwp^ zTmJq^pB(QR@1_(Qo#|N7HJBc;Wa*nwTBmJJIvbd=onuUP`0p+t%Gv$sNZ)p6OOs*Xw{lc@$!w5=T5C7%S#Ky z9@yk9{eZo&gj&=>;}Ot(dx;K^Mbe`5xx-@SUu3Ec;xl()?y5X>#esG{55uWpN%qa@ z=ydBJZN4N-2VJ$9-=J_%Vb1vh*#;CKr`O+EfKv1CThhZsK2}u4QBIRVw=lpZ7;?}x zS!}7|X#vIUJpuFibMh|{@g2Be!`7hHto^GbRlBtdoV@*3nU$0ouE#YebMpnRgaPfP z^$QLQ;NU@7GGU39N?_xp)sSE5(WHSj(C+_UyQ_!-)&GRD+)%$FcCc9m^OWYgM_Yf| z?Su5jMOeOE|7$DgaAU;Ljq)*WnT2bt8z`_x7k>$KH2&au(V(!@97!J?CYtOg{ZC;4 zCtFx{Le;HE;SZTI!4-~Fhs_8wOhltnJDIUQYP-qND)u5({Xs>54k1#Z$OGIc0u=!< zcRTzmG-9rE^Q3ua>O!w&%{*tue{>IaX(IR8snyf}o2`B9=`>rX*~3{j2hJG)*#cdb zeAoK-?bMr=YGJqnblKh0pDB$TfLW5TG#VhixjQUjR`LzcwsSs!vHBIvF_5tXP()AH zkh^Qd`8q07$k7iYaD3V&2~s*!;|OL9$FGjG(@x^;?N^OG8pK*l8 zHuI54i>qy0A`B$Fq*9wmPdCLA?LLTY7|emulMJ6*$fOd0a(2qd$q>qvN(PHKqr$Hi zudMdmmy&|UuNRm64ROFF_%dRI5PJIbEDMm{w6 zQZxArbOd&qf)u%9dDOg^)Z<{57OB2Mzjbe0Jq{EG4#34QK%GW;#OiAD#y2a`woCF4 zVua(M=ZkN`9QX_xa#V0s|84g}5+|Nh&MWA`p55xmlzX>3cNKobSizg}#KdrT#Hj)y zv~YSIKPwFPcwqgU!y5d3-vTwYIkABrOj_eDdPywiiW*1o<@PC}kBIMZC@ZZ}!q4e? zx?;*}jn8W>W&EiRGmL8|NllQRq2!ld+3WXnjpDu?In4(S(9>uIPQh1F;dHVx>=beG z!|7qTIT|(qxZ5nQqQOL)ZnFD54;9SbBhp9-LlUnKeEnSJ&6YLQ(epJl2zy7FicH&(7A{+y;phV^>|1J z)UVFAn8@O=9VkjZXX*X z&z-wh7IWqDTH6?Cn||_Z4th-RKrXaZ7;-k59d8~{Gm;R{MU!Q1aJN9!Rysn1OpA;V zxc0l-`_SjCiY-!$83NfpkJ==T%y42N2ZA8T@kT?JsPyCnn4Q0)6?TSpXGIYG*-dxi z_S46Gz#o+}t1)jL!{L*IuSVZ*<$}0JY~!~3UIMLsABISwr@rr(YhV4oxxQE}9SwSU zPA&SoO%!&9hD|E4QVS{e_#eUEbcMi&DLD_afBNHs#b4uqSunw9q?H$b2dtfA5@imW zXxl(KBucQhjD!RYiUL##pfGbkI2}Vo{MMdAN=9zkmKkD+HYp;ud*QerA z=9SWbs)jrqO5%{;ls*J0NmdXH`j2rAf%qhWgQO0CpRmL*#t#!)Z=Uo(%^vvF-*LjV@)yXN;6YlV{t;tq*+ChDi0{g{1`s9`18s5@D$2*ijU+(z_vCtcK~{ z&Qq%1@$%M#Cp0S6v4^p9v?vI_h?!sUft5ZbU)e=}X|DlYjcz>ip@=OMzUwetSMzt& z4;~v5P=nky-$+=bQSOSL3)c#?9iG3qlSC~sqZM^KNZCCX4;F_g_%0oY#70kgKL;KN zHo*suv?jyip% z2M*|p4_GnjvWDVF`R>KaX#@J|7$!vA_ulV>0nF5>Ti@1 z2Qb%dohRhodkFhnvmP=Hw;zkh3edaauQCNG=Oj8I;)xriP`Fa98t;;_9Mqovgnl34 zCPeIjS!vJ)u;)nJR4tF*=cv$C17S^e0*xUgjkK0#Exf7^J9c2rR~XlA92a~R4j-gQcAM8@ zAS4V2$&T%c&U|q&_I9MPvQUL8?7lhf2JsmJ%USz`kyG|TLzKmPEH?`=MfTPIO_9Z0 zQpENrn*r?oFDTi{M8f?HSq`#?rWzYqUXQo-mBtU!%r(6*muEq zLH*lLi0(xK`~;*CQN_+~o{orK5JLj=o9%BzRx|D_9co7-UWsBr*;m)XNv7fKHHc0~ zQb0DOHN=j-oR)BRdc)$XF3Hu9t-3@I(e)~Mh7VRBB-%J0Oa?W;hoi?p8RCwpl)H0pl7iVhMf>98 z`Ub|v6x5V@D@bQnW|N(J_6_o#$Hr{zr+4XOvs;wKWQD1-Bf!Pfa?o)K_`6D3CuxT2 zDSmqIv-dbLqkZ`&$_25;vB}@C-+EP%OWL3F>+yb!&-ys|3Rcg_2>!rSNT!IU5b%bH zjE(*+S&E(qp*+$1FwJ+kQn72i3|5&luzCc~(T2EQm|#N`uqy~dTo{Y3z{Q+K(9ZSq zEccgr^Q(}#%(ly$m z4711P%wxyX%7N?~(bCu2@h9JOf3kQ{Xi0VR((@5o^(X$MXNTY^<(06l)sRs*?C2Yx`5owvv!->I-AZMwv z6ko31kvOPq(z}s<6YcATwP@hzv$$xYd4 ze_jk%S^W?2S4981+u$jDs;lRIzKbNU`CFVE9bv?q5BBfh3uZIm($Iu)7}|4jiIFzJ2e8z*m2VWKJ`Pi`!ojolRT)^!$LGu_0lzX!|SAAHhid zN!`Q4xV^o-pFe*N(W&f39Pzqd9_V9IGI16M-WV{!((INKhE;9t?F3mO3^7GpdJz*j zX9r5rFtx2KE9ddWePg5|(C` z46)4ljz@v}0ug2>C#DOyQL|J;bESbXpN)N<%evvT#-wNtG-?l+-fS&~MJB@y08OvSNL7{<^Gn z-igqWMlBy1(au)`6F_JqMBQGLu&%By#cbu~Y`cIM6E8I-1^(%i@{ABaKb^QZbyiju z{L01F1*G%~ZzAY~!-3aa<8{TPz$D0N@t))!1qIh7rP{RF1>E&p^^@S(iiXxk$rWk; zxqnr&T5+U1KO=&Ey@F{7v*<0}tvYj+zt&;BDN7y){I$(Pr#TEwCPb~@@rAoJwN*H| z5wbj$k+kK4i+`GAL?LFJdXa|xi1aQqLCMpzfeWf&;a?lfHZM&X1%KV%Q27`KE+(cn z7}RO1WOo|r=(&m}z6T{d<6kTSUIl@%177*zO2s&HCW=O`he?wqevj{zYXbD(dv$*l z@MUEU!jlf8ksZR4|1!TB7532XvzrPb4obtBAxP>H?|k7`j|naT9lLvY$OHz86GL}r zgx)Xm+~Lk}ckz!8cSI0=g*OU3mj}!8xR#w?3$8szx9e65CLNt6O%KGr(yFSe z63epDG0#Ct-*KC?lEuWt2)l3LG-)BcteBSuUBOzQw1PgbVO2ZI@RK$YLjt(?br8I4 zG3{rO1DePE?^PE6sR~7>_k_BYB$wkuRzhS@_5-WcyV1v~+QWj=pxWTM(t)75WhRQNZuUOABy6-wdPCq zs|V)exbP@(Xk%l;ASp@9N2sYY_xJQ42@G+7Cn`$8qL#fv5*Gos#0X?|k|x64vOQhO zn6AK-(6O(V1J%Vg^4sYO4DAbdl7O!Rt&<>zZvc6OJ9v zuC+b^1hg*E(gxk4KDIsa2Dp4CZ46os6+qNo`Z`Km%PK|$P=P2+$eR89`SYvag?(zr zkDv$9?CfkD>gITpVg-fdpQ-?Y5X4zp7<^ci+LFxR(g9*@S`(Cf4Zu>!C{7V_;zoqvRUUu+*q8=j_hCq<}mPB~V*|t<^ z{x#b5x|tmtH7N-xgWsawVryU=#jw!dds11Y*fJrHQ7gt`DkJ3l0r>21^%MDyyz zmZ@i-(~g;4n_{Z0T{$(BCWHCnMRMX)eY%60Hdd9!9<{)3+laDuH7{FHe#9xoS1kX* z3)cikIjBi}z+N9NOQ&8O^m@H}dbcZVj_^E# zpt?#ag?uzyr=A;Ub%p^?5cKJh6-(~NYLAh+k?C5Mv znyc%zbb86=W(idFnjm5SOG8ZTe;5)3?z``cNr0 zJ4!L+^}kz%4s2!kq0BP``Pd!>UBBLib(#c2Zu3o7+jaw87)Oc9ra!yb#Zk7;mHr&T z9r+-!O6uYj031=j?Ccun&YuuKa;h21M-49hhY_4Tc*h^w6{tr+8kEg|)EM}({*SR&C@3|{ z-E-T?i1xIY}&`P3}Mhc>VECet@zIc^G zIk$lUvddq%+<4_bIm(C{SyyCIPu{hXarecY6v8Z9XWM}Vuwq!Y0j8a6=h3=FtM3U_ zoKW@p>f#H|5$!iWG`rUA(eU5vxCFO{`8U<9Ui`ATzT4KN=gEmnT^oe`7FST7{%Lqh zgb-8oZ%Nfj(!oGbAQ%REmRBfd!ueR)2_=JXqqm~G^{OSiel8j6eIBv6*=1;TZtZR2 zHeG#()KeZZ3B^kOQ--YYL&VRds!S!Rlp^t1u*>ytBo^HlU<51rR}=|P%DK^?S$Eo??^ zk^P!72g99xB6Cgscp|xdj%z{*GvEJgyAqRv{GA_|E*utWqhXUx4|nI={JFQeO{1(K zg}3TIqA$e#dfTI6=b^2b;T>v`4Zo82TW9vA5W1?Cep&*>D@vOP&62Nr4o3CwH3MHw zkjR0>6QiTa<@@?rx7!nWFI${U=WDhy;u@cFc|9cT_GO$OD{A`1qY40}h!MjXMzr+N z&{sp0IiIM*RESmJ!=~p8la_=)(ebZucRsb2NY2Cr+L1Oq4ziRQL+4|D=!c*c@!3Tm z07uyW#JoHb-0No{I9y||gw+#@I0r6R7|qtqE^7j0FZ1+C^b4?E8(AcMxv^j-TD82B zjhRiVyx1JC(Y;<@$7jk8&9(C@(^gq%bVl@LiR0HXvY=jlqW_7b_zagfnB?8N zIadYE2u_%LdcKH?ike8nB!`1RCQTe19J31x z-Q@fG`?GU%Qozf55fKr@-cGt~q5#$!GBKg4RjF;eG`a72aCBrd6tM93uUygCAGBVD z!5 zDw2$hj6d(25Va8v;7hhW@98N;vjUD@@j9exh5G<6ZC=^P^5b}dH2nJ zui^xl+vr9u37!2N3E0{dZ#Cf-!jq;dYInYr6NptmFkhML-K65_wNfP2PN2kSd+- zO*cg~;_^hJfv_5r8t0eSih&IV$=f2&T9w}SO$>xFZ^XW;Z85SO<)~qVcZHZ({CkDf z%#l9yZFrFo6$vj2#jlM9Jshda3{=Rv2D0L(kSf;-oB#W+Hh<3X@rBm!O*dc!^!ytq zNW(4pyB_BsTYku|F(Ix*TFJp?y}UBln2-wNx%tV_$Zqf@>tvn}^9DC1Q3pRoLdv=z z6`pj7M!}T*U`$rZn^J?S@iQ5>x-W=-c>5snKobQ&#Y%AP*{(O>+I7zxm49d5VidV> zxP?C79b7uWmNLlr+xith1uoKItzJBYZTj~b!q#Q-J8RZ)>G6>OiA>~=8?)Bm22V~7 zuV_NyWhBxL4HR>eGrJxQFl?bUC1ao+mU%2mY#v0xn-CV~c=l5=E{rGIO!P_~qqN&6kn42NUsw3E z_tr?Z*mka1$JwV>x@~tZ)j?D*l>s5*Bysv1FKRprv-v&(nhVER0S}31s)mnbdMi1+ zRPnX5I}H0D!>re9w<=4AHr?(Tf4j8cOkcF0ELWVI&Ey?0QLgm3;cPJ2j737hfZk#2 zB8)n>drz^fMoy*UL`Sg@@3++#UCWCZgI=O`7;i@XVg$Kw+r?4?@mp|lu9PM6bS3ie z&2oUuaP>_${T8E_AVqr0@UHJUk`S-O4&djQ>$Nj;O3QLAQ@(oaQ=Zg8mzE_FN6k6O zfGR!Cx~F@qZn>h+1E8zv0H9Y*{u9nN15n~pQnG+U{0@};mK<5wiE~cq+;!D0zRWA{ zU_mZ62*$nd_rdYkXo~~UpOcHXmpof_2%W1UC@vdSikBkc(kazOY|ti!GxdVWeDHGB z`G^1F`7Ig?YgtluxJEf~K=Ky*tY?c6Uso?9;~lNcw}SH*BGcpECAih>cJUERLlu2L z=K~~k7Y=o}NU%ATVnu#^pyu8d)&FbXz+*$!r`6Bw)!w!IA4K^=jC!BzUagVk zE|rmS-(&M0-XGy~ao}n+Nj%q~3 zdnhPG!T3|)Le7#MuKOc#Wu92Tg*@&29~xyjOoIk!^UIuFM~GT{W!IvnFSTjfMP_=n z9!+~^8dHOv@NdP|2Hp;azf`orJ!zWRQ#?!c(+RFI3BIprrxlJaJ!}9tPwZ-8Z0cPg zIwu1cuUn_=OX}u0`Ea5Skik;=PDXfyj)I?%4v2iZV%Z2@0r6hb;Lm^2Vh4~i(<6wq z!bRAzs-EF4F${B>wDM%v!@#%Aq|*7zZz4J zFaY9R>EH=oG~yDP+?1ZZjFv}Era5Tgd;cE%_ZVGbzm%;b=X@ot4(}uB{HjyAn@_m% zDU1izmOp3!uYi+dn)Z?wFG@#5re-U4sQwh+dMXCdoG>i-C8_FPRBB~YmiedEPNX+TokQRDBJ@++}#c$S+4$dv5J=Qi!{J@#K1H(Q^G zZg$QVJ!g!qMzTe6u}u$!0vfonNjN|6tnl7`B(_N6GXAb5k|*L`I@9UE;OpuWVB!(C zyKc07A!9|kJ6`gwqxYih{LEMQW#B7oIKJ;s?_HoCS6&gN>i~*inyp@v)Y@iILNIorlQAY}FXgP`kL4|L3$O4P$vp#33uD2?q=y{BWQM*myi@+@m^ z_q#lI6+{a$rK7)XQ=Z37Ou(2{j#tM^+wV>U*A@QKe`+K!6j;XwjZa#%S7HnQb9bo@ zG&6aTe+Xv6&fTNl__>_W%Vqa78$6s})*8m#ViGuT&n)?YVakp(d$YL?MV$)8qCb%< z90Q*OwxMUKLp|tP#JH#Euy zhfDj;zI4hjfx^Mc(+^_H)iXN%9h38AmEY$M=bpTrW~xfCAO@Ao-=;CZVrbwWH2~m_ z42qiVo8sSjlFN#;7|L*Pgm`+znP=o*|6nJP;GSbXWH=zTE7RrhsY*iBU3jBZT9bc; zmxrIzB~f6WhuKi%iUJ0}r)VULqo89;|0Zrf$%^}CBu1)`NHuSdql`itz!}sJXXpb3o9=i3Yb^0W=9Nk3z1K($N(OSXU?!I(kzQ^PCU|;Qc2*W^j$|hRT zqhorDcf9Nx^NOHMM)d2EW(a^uRQmh;1WBFnVulnE}<~R7oyH@?9;J1Z`C~pYdGWg-sP)IGr zQ&0#xeh|&lXkeOkr*$)4vZ7g8$n`(c)@eIao6Ep9i|*9kM}s19cWqFKJ^X$()LdD^ zP8)QeS!aFdZ*`5vFfk9sTy`N|d00%}?@5cq^SIdO{ zzEKnPHPdf=*CP1hGTjd^hnQ!Xm-Z{Z*v|-Ql60rahzI6;;rENe+ms$ze0`qD1XA21 z+PTQc43&GW^$UMYljRR{Wt1NeGV(GR^n8ZTvfbv()2rB>ku)~Bu5f|7_ayDkGY)rd zHy=0}2y{Ds$Wa)HymwR|%e2-F}@yo4#~4|Lt{$t$B(1RMC#U6Rsbr&}h zx)0MhLA@b7ee?|{I8H8K{iOzEWM7gZde|Cp5!O-S;%3_>h=$p!U>Qd8jBR3ZmWrCcNr#&W#aq-H41+7e_8w6mGD<2V(+wL3J<3}da zhMq_FB|JE!VG=O|EVBim1M#^h>RCcOY?~hOr_w@?XExL*ED!RO~1196AYsyCMTH zohZZK!kJ2BMRh`6EnpwI)Nkt3gIussr&(hb`QD|ZxNlg{r~A+x<@_|Uk0IVeWXuc* z*e>w}60Pe)kXhLI1*_Q5=I6IxD$Q$KyTmg@jmJJsRJW>Oj`HrIiQiCDV7+6a_TOu} z%3$d5uV0sl1Ii$Y;qCmuBdg=wfl_+r%YX=NUhmqKi`u!;u!&NgcUicu6@;!ZEw|yX zzv<^iYZ0hpq~pe`!c*qj?ARwIcl3QIpOBlcdHWveV?mK$)HXJ?=O2fFi8;bqQd zRvV-j1*v%j3DLXQnJ7KSNd`Js40N@U=O8=dJCN62my~do~j4`{R>=JofD&bNbbs=t=*VyoME>J~wwRW+VZ*~`jb^kcm*$KTpxr;vQpf@_n?Y5>qncx9=7Wjn~c!roLIohmgqnar?t zDzXz2#?v5aI}UrD*`&P8DHh-)-&J=Tp!k%ElwXzQh@Al&g@?dUqu|TmJyOe?K6PvAquZ|8CgW`0`VG z{fGI*+WMgP5c#{MruD;$(aEXJkdbt)QQqFxqs14S`FFaR&LaUF5T&;1k(yBM>5JP# z-v{gJmk**kBu}5(>d*I$4Dj}jQ`dhXXKNq2nq-cC|3|eW?p~5tpGU0Mq1mQhbQ-Md z>HBLw$wI4rLC(SK+g;`P(f8`c$lL%8cTT$NI<0xf8SXdx^^0`I_*su`BEEaHL+8Cm z84XMOG7h%dn_s?-jL*HRn<4LOCXyM5vwJ+AOy#ZPhn%%AjGX+cXIYwNFXghQTZ>RKmX=>Lft4!;-na<OuTLHZW zb3oW^MRD^l`NT~Lc++0uX{JVq5Z9vrGe3O}e>M^+h1@?9Zi--e>NDdak*mYDybDVI zgZD7|+$MFjpx37^Iq!4(hZ>geF2();|75H4DkZKzq8nZMRSUZDpMT%o7Rq#w%(3|) z0Ge6$G#QulMI?YE_FH8pH6D?D6$P|;Ep{zyQTIr41+=4570v4Oe;L`6oLGCh2~2U)BcMv}32RZ+{% z{l4W;XP(e(d`@W&!dRS~5w9EEZ`XQ3+LVqhZ=w~L85K|7-zRi+BpQ39=Duo%?jP@D#pWp6mV;eE$QRUB+D=e@B5OhiCT~ zK@Bf3$=?KJE_r!^F{GnRGb1M7 z`&cBJ1Z#36UGrk+)T)#(D948UnUMYMNJ$)&?RRTuCD;F3ri8s{7Hgy|9<6^Ko4f4; zCv&yK5#}5^7(pQppwoXg_H-NeQVQdB<-ZycCq~WQ0oxAkIO{!GaYG?^CK;~+ALgHE z5>I>&j9Y=$d+zNjQA);oY9AWEexJ^C45)E;6?xYe*Vw`fs`K8<4hf<}0cTD@6c$#g z_T_Sg5P#ZXzT~#eD{oR23EWa`H7v5)J+09lmGOiP@lv3yc&%`8z1=nG^+4YsATEgy z#cM4?2G1Z!!KQUzW?PosMzr2oY|_l|K2!Z6QI~YGi03IE;bx zn0q9fz-3OT?uL)J(rMQSQ!iP^+7R&RZ_=7K&T9rz`%W#LaRCp*aJXD-mWj!$LIHao zInTWx=wOQhdm~EOw6yA~-nT3XM$)(td6v7wbPm*Go>>*PZA?^JYcH>~a}t`NCSAah zaP>DF*8MB0sLSy-Bq&zmqRT|W)RWubGKuWRDMec>b)F)ggLOF!@fKc zH6#5WvfctHt}YGNMM7|Qr*R4HPSD1K1ef4Wg1a}tq0!(FGz53|08Mar_u%euI`hw& zJ9F;ZRaDn5_OfreJTI0!sn}S+s1Mcp$3RW;hjIjv(r@l{FpMM2OkkB+C5g6*@YdA` z0zes)wokr`^{UN!8X_PO42F3meFI>Mm~>rhg6QvKAlV3?>3gA`UMwuX$xhN35P6hX z6vJplC2T8@pu+jUuwLQ}swE(H8z%R8a-xW<9dq$Awx(T!$_AOJ!vrUdE%Isw>IS)N zJD2Lb34dvGy|S38MY@1>h7?n(5*ahf76T|Z%%F%gDTbXNM&X&bRNs-*cLi$7U$(@& z+%;4W)%GAEQ4*OO_q<+QhFDb!Bww@f$c6K)$!SEdWEf!cro;B-a2;n1-0)P05S4YHgkc8%Z7dzdUqPgWon zuB>vlH%}Tsem7&G6ViLUK)^LOj0?=znX7&b$aM*M(tpLhP$sEv{q02>TI@WRt~4hO z&MNONRuJBMra=7stMzYcBTJB_p_%JIAe=btlt!M^JWiKcbt24Zo;Dwfq~%5=PN1qG zVJ;KhC39})y6|PGbLPH_=Vq|WUCtzQfBgA3r-RbombcFWV*DnV@5;3}Q(B{`cp*&q zVr4e9YTBztO{&}O`l(&16!+c_dZdUlBILpPMMP=7T?AXGuF03QACUc)vtEQ&;ggP` zoAJ`vn6rxWK>_<>i^lg5KBW7PaDZ|-RiA2+Se2~KyR4dhc=ev~%Yc`m2e%ExKJV!& z&e`5DBS+=un_G6aupAd%L6>D#i=0b_#)|2}xS+a&7wP-?M0%`j9)IOh&k{q#BBxcjy5{4s|S2>TWdNCVLAjyBo+ zvpus_iOuMBnvXf;MTkZ?o%_amg1G-j_(mR$OiPRe?b|FSWm()J&vKT{6Hel@jr0gy zgF=k(kf2Qp6{2Dx%rX(5lkLGoWv5=xx-n%e94R@Dw_7tW%VHKapAO`5U^6G(;X%VL zLY{5GCsG-OQ4xHlJBF`I>F;m7K00H?JoF*;joJ%>LLe{VR5ibn$%ow;@!ynsqJTBfTUJ)(zJwQ?pxo9zo|)2J=Xf zXkfS+fgSEEldA8xQL7wo9tSUfGH>d%nXG80CD4nb!}yfMPa&kiT9zE2qt?btLON95 z83J;D@s5Cid_8XVgqs7A6#o92s-28F;@Us5F#AF}_N3_TY0U$W^%gT#^Q@XhUt;XX zGh|=BH8!%ebk5dV-;2(+i06P8JpxX`i)M78-3$Jlq7vNZJ<e!RL>#(S>Ve%`Vfd z5#D4R`$=3k>69#u3u`1F`WmvmPE=Vor0s6S)46)QmOn2 zyuPA~uQk&M#ov!2%|aYr;*`?_1zavlKeIVu?55&B&NlDCdkzT(Cl#C^f;7XZYsFsO zg>RU@8F_`yacT|g<&HJzmm1qNn<~;epzb1gb;G5j7Qj^e$&T{B>WzH;UW}hre2k`S zfJBo|t+w(&5FL7iY%{&BSlM>ucI3pujx#Z`83V%~b@ZE2M2+qB)ySc(8eOvCJ*1KX z$!oZd@V8&3ujT{h%X=@=bA5|JJ+x)axys7+IA{OLB;2pj?*=eCe&{~GKC#dz)4?Q% z&@Y>0z-sFSy=#9~GE8YSn0sJUeqx8wD<5N}8G<|FDZM>0(0I^80KiMnP8OBJ%9NMC zAF1#f7b)hIXsR)4MF>1aTmUi!t<^fFnZYf|AG3+6MMRq`EjtKq}uugB%oo8~PQXc+|*{q*299GdU zr~cHLz&^$q8>*3a{a;~_N9zu#1We!z>rP*k1ne4OT*hGlF_r>wrxEZRPr1$4r+;9EJxJ@ruP|IOVnN>;GVH$S@Z23l1zJ&S zY)Ug2rUL24h<$IsFv8GQ;glCky~7ngSMAl266W2TL0I#cub-lAI5hZhL3R5O1fu?X z?iRL#`4~|E`Qbt(x8HBh8%f^eBqV_Gud^uQXZ3**e>K8!x&C1Gg70e=Xjqfsf_X|w z;_4t}mtWX}6vh#jXZ)jS&cs;hjI5PrlrsWXaKEE9-gEWq-t7`?=|=NxiLnKbXG_?t zGZOpUpg#znG-SKlUt3X&v%X6J_U`xU3At|%b9UXbvyd;X$q($dw*4a37QpBDDK^W+AppK72j0X6hF(&7F$&eDdt|mU{t@c`3B%>{xh2j(0+>gc#SJgi z&3>&Jh;v69057yvWhH{-PtCHC_FA0_##?XmT0I>W98Eiuwjvj-@{1H9hoHK-uUWAl zGFfG6X_a{m_&Ao*JX56@3m9C$>;RO|6gXN#jtj|aoG9);rSFQWIP%|d z-ZnvcC5w(395~|CxgS95(fr8iG zd0YH%CG!sJvB%FB7JI%eKTXLHGHcJHkIJZu*LE>_bG@6^Z{}kcQo0g%PxpqBf30fF z$1doRbd8_z4BFOvDCY@JN}rPm|Im^KZEjjuW_1{yH0?QFGwron|DxX3rT~@2nHg>G z+{wl!K{>fJk@cv8{^u6oc-{jK;sw$C_Fh)!M>_VAg_lJEE|f;5+ilWIzbRcxM4+k; z`V?KL0~9W%|6XwZyIzIM2O~)Vb2bA*(W!)0qbr}|-z;)Qyxwz|le88P$*ECz`Rw-m z@Fq%C)mbIyvVsjUmgv-~{Ul(NcV|4FKN+_&UERHN-D(FyT&evitA5mS7Lextw<}Ulm&0Cg>JV)=^wKen#5nZTlbZc$TBMhmekN9Y)y9W z@8}N~9a8Hum~2ks3Oo*cfwi4KB8Iy4e^$^g;fN**qp?RjR6pTZ-4~uF>G<2}X9STD zE{!VtOmdE_m&TPF#IB5o6$P`#40%0ns4b@_$9)$6$<+AVgUO5#R2;ONW%5Y2eYVxZ}Or z&e!zomr`Y1t!$x{?u9^yGv9iz(ULQ((ns-BbYckzjHI3<1Q!mVx+nqs>0N+zaRVWv zKpR_sNo4wS9_7KC-j{DR1=+g5lh~iz(aPonM_{-r9DuQocHru zS)#LZeizz?8BdxVPj%$Wfdky2Vj51*!J2|Y?zg;l`TA-72phxiPLdKqcEaIGnqPDm zjVs35%B)0$gk6wzC4eP}hl~6sTyZbbu+^SM66&yO`H(I54;qc~@`Am;I>l5!(R^oX zk0iBdz&th}ISePV%Y7a(*L}YYBk`i&Y&)j2JcgB^58WiuKtew5oVs@C*_qG0w}Hp? zV`&#nuc~)^@}`XI2m{v_k!WFBRpCgqv}CWXYe6&OF5C_dbhY!y5(rif0&C(@(au<6 zCD`gCEjZnvX;0R%fR-_-%wYiVNYD(E1#H(((6zCj05aIksz1m$J}>1g{`jPJRnin= z{~Z*1%{A35H7IEDt}J=mSEC0-4DZ&-?1&qXH?2ByQ(#V+H6*Q0|6m?4{fTUa7&rck zzJhd!2>1?{Pj(HdK&hOX=Ru~+h(dcn`^*`$N%)prR5q;^>0_2dE+EcLQLK6be;B@r z{~EvAe6@e6Ht|_@2P6wCe}nDK(C~$>8WdDb_I68m**mLm#54AIDUlh;_oD^f-L$#h zk0bRW`n@$*1xgTo>8CWVB>@IE?CFj#Yi2TbFeoU?GpXfa2|>250l-Ajs5r@AYwD#g_uM3ww0qsM)oa=DaAPqY}|7Luu2)BUxonKh@V08N{I~s1NxA0l`Rf z5>PxLF5Mw+p1=|I;M&)@5?#_32o7+rfw+rS3k%Ov9f}$K?j*9EtZ~YdyEl z+aFTTttx-zrOLfR4hrC0(2hMoIlRz^@YJJ%4L42_d2Y!x5HsGf+Pk}hZME$_nEk|a z3(`8+rCK1UH`ZVf++~wT9n-qpX%Sl3wADENedND$YE*+FT!L`Z>{rt`9n5ynxKQ!5 zMM9-~&#^*GH?T^Fpnid}IQ?j(ZYYIQ-?Civw22aLcVPZJNZ_?=H{-3v*vTltRI;MG z8&Ez|{IvAjJL95~g}G|_&QV0-;bLqdMP=Y6;#p0y=IS9p;WhW5bFuC}jt4aTw>X>< zNYcyo&@N8u1tPqW)C#LGkpRL=kf<3a{5eXgC0}s!kAAyOr;nRKzVh_lD>Ab3yFD33 z;|h*-eebLST~({>dodS=y}&uPSoy16W4ZMZnAD@)b(Q@Fdokeu7! z+*Muj#dMYSiJC7i>iRH49{oO`B*CZH%Z;m*zCGc-z1Qg|Zy=mQ@cdd_q0fox@AM%$=;?QTn}>eaP_oRx`Nl zw4LpP%|4u*OPkt@+-zuF#))LQFtyL(8jm9O2iQzFql$KVrlW%!4n*PgDS*JD7`vS? zC)&adtcro$LlXR~?)=B)A#YX6-KBAK9(b!j@;zCv#%s1&V#pVas^2&p_dhvYa+4r1#w&Ue@2{!D0MAJ*b;bXYat-;ctV+`^FWbbUv~=EI*p55WBb>$znbZ6Ft}ZEN-8 ztikeV#Se!LD+YuYO_*@JM((kuwE)&qtzD zDyV1n_gD)R6bAeoL$KRV5JNinq~YETRL<<`2In@+Hq@$8#$nlSa4CdE(?lz6%U==| z){}n&W?QA6I-F#0I!~1v7jtr?v~%B9q4iG5sA(8(Bk#w@kj zW;g(nwwa0e5jErciUV`3sOA$X-tTMxd#uwoUUxjI>A!dexf)DtLI>_{?q$Z>M=kbu;*$jt0TiLfHvt(-4=T$a4Gr9NZ7F{gg=ympxszz6a zv_2>Ct+RTtEM2lYVR+V?WZK&aGZi`VEl6sPbXP|bJ&LkU!+Y(rVHmZkEVyxm771d5 zL^N=Vi`T7(cqflcO4!PozbJa&1x<2%3smx;Oo%)oNy!ruc2J&%6E6L_pode4J8xHF z>DO&Bo8g;^*i9T}xT~jon^C_7Cp$Oqt!op7!twM-DS|$r|Iby-qbDTZ#H$dVZeP)(3P%?#-Cy3yKg&)G#s!C)|Db%KICDt6t+k0UD*9VbWe$u2W@JkFdk-_q#fqACKC?F z7`!q1K>_F`m&|%*oCql7-;iEGTIy^89>BF}EXZxZWbPZSf0Bu=3%l&Wqmq z)z<3!>d(vxQ3tL^VQ7HUUgB)=4O$bc{4IG-_+(-mv_`Q++E=4kgmo8F=ypr zaPI~cRFm?7c7??(OUy5QrPYIB)a>fpoD9`=0?gUX@ltejbd~)Am!+lU$=n!6nhWRp z^?rWbUK&zN4eEuVB6ZxW5C> z62Nwd6y!1p`!Az9=DF-lCE@$7|LCd;;X2aVMif?oK!G|hR5i3Ui6a9qm zOqb8zvpqlW*#Z$6YOkJ8U~+|k4KkKfR7*I`U!f#?!G-5hxVyyu=hgRs&_=3Ark zb~c31cuz^(#CCz=T37Mk)|9#h@LW~Z_%SzpZS&vIH!K5zV?C%cElonW?l#em_HTU1G` z`g+U*X<4niV%-)=sWD2|lNp+d^oDNzXnh0YLQ(1bz{ce`c@X$>{m4ceg4ZmCI=Ry< z^Xn2r*D|Uw${&Pme?7tiSCpD8*qSzM-!3s#RN@p2u4GEFAC!5nBusplMVRua0sWII zu{08v-S{ug_g^LY(Rk&IwuHD?Yx3so$eoCA>#2+tNT_a?R ziH)5cAO8f?U6^aQDbW8MbrtU(l!-~woZ6HkN8ttf!~vQ7qEta@$J4bt#_6W-o4CDa zKA+{e_S~;$8(XmV-HzPW=0EPQwLM?~->PybKu`7hR0TRXO38SZAE#xgPa;2uNHeC~ zChQ#0^$~9;*TB?E@B%+tek|9`JGKqX$A@;|ILO3bq0?q&g<`=cV2k_5K;_U8UoN;+ z;j^~(c*7e4G0fhN>mwT43!3YR9WvZib2C3rY<9=8xRYS?$xtqIpUG^qzuyz^tbRRu zVlht_I{)?Eoj@$=efpqWB!9p2DdXz_iFy6VnEno6F{qru4zJB_o$eH0@Ua^CuD8%h zenh45@mwXOJ>aWok*z#=z_}~`QoFwwK@M7w6^_x3>xI4xH+#yBqR>spq76xql+`mz z&Fp#qJ?#0!YC^a~uZQoN1O)Gqiui_T54t1Nft)?2ZO2qrYzrskOV%ETTK_PLR&MQ&P~ht7-GvnKv*Bq~C|YzZld4Mhc?<1$c9 zO^u64kCs(?eKPNC(6ck()ju*Siu2^$Q^bC$VIy-!&VAxA! zS=Rd_<+;f(96uJ8s|+@FAQxxeV4ONWl&OMIFjP4cUMxl`WpK=PE>~JI1@=@EU2%JD zt$zcEC$IPt!_ZG+_2Gf+2QhX1ItrmGUyOSMV8>hxH8pJYs{QA!VSM_;)dE~3*wGa??7@1MBJ#9A`jUb zhPal5Ht(V^f$`Abz1Gi1?ZqDZM+Tq0P0bm%(DTkhcs)HS8dJ3o6xzpi(}8i4JQEY= zGg_d$i3S2aeR3S@#GeQN8M^x zh|9HHS}?YHOB|S6%~TEb{h=YF?=#OO`n{uygHL@LF=ue8mY>7s53{gz7DkcAnigAU zpHeH@+sX)s6b(Gf9nh>qvDR+ZwT~t6QyoXIM_OQiC9MSJ507|=C7r^py$=QdEd~k- z!O)BP1nfr95C{+r6O-HFe!kjF2daPjSJ2gAsXpv+t82#c7X~#0##M5ynw)Pe7`{}VqmlTh#nC*IV~;X#}CH8k_rc2_nL8dXw7Wr zWAEvPmxuxuQ%CWux`sv?JtY}gG&~~483p#7JyiR6YJGQiB*W=qTmBibM7>0lIa#4l zo$0UWF7!l&-KUV}YW!R+WH)Vg3l}{{{t?sH++aD7nN7`T80JNRou2I2%62l=z zED5n45zMFg&k_>3N!5;COf)+>Y!0apLl;~_%SXM_L!vy0Lo015JjW$tsueq8=_QTH zwUmN0>2oYmmE3fC+1<`IavvDSc5jh_B6ndZnj1tb2mDztH&W-d7SCJD$sb*mMnbnX zMPe}T-p$Gr=X@?T^Jy*5#XRkLkF{X8OK%y|9u;2{Tib6useRzq9A{Q_A2)WH9(U`x zye^b{8|=^6DO&G%6<5MPwqBg%v|{l*)6oNj%+Kq{vXbmI9cf+Q9cxT<7YQub<78#0sn)sXd@GgkQ&P$c$>v;pI3}X_W~PRJ z2Fh!*%lPG5M%gzfoS%at0)a;daOf?V>kP*4>_K!L1#`wHx8c9XE_I_ z7Z=WOW9;kX6ZFE^_X-FXyfPpo=c#E*o>{49c|A46u71I2t%s|CiFQP)hZ*+~J zNE>?>6P&L30aXqm!BIQToC)wMm2)sU)fdcJg2;`ZO=Y3Y&Zqn&qiW4?`|D5w$$(Lc!R*vwxuN{0GK@3H(y_&aFJ`Dn~va| zQ-w2?%4=8vlO%B9!h$G+C`?V=nJXxZ5JY>VU&TS>72tzPLxHLEr|$MUgU`*;dv|xf zA@ff8U=G+skU0+1Afs$NKWN)vm5nusIKWDq5N_9Rt^f_UYIbH0o=SqQEc5cXdHIND6<;uw^w^$KoALfnF&8ehFXwc(@biWL2%j0JJnz#*0lRi!XtdM@vKgMN+_E@!;-X#JT^$ca8_mHm_c<89 zsGpyY4-PLM9~s4gpTOTK$N!supDQnT6dH~Uclox50EA|hl^Iqs=Ill=q^o(7)F2mTT! zPhOfVn8C0G0PU%u_I)e@mzTo>Ug}_-p)jZ}0;=!yCFfIo;BNv18=Fs`0-3rKW`}@6 zL0Zc$YU?x`v^2NA(vlr>%jQ9nxPuTzG3){A^~7t-OiFaj4gDAYXi(dMv#uZKM}2b{ z_ZovdnzVG*F)I5~5#vx9lkaVv`S~GdYM*Ft-;>h zIA*kNUTF-xx%t1T`N1V1#hR-Ed!*p6+kWvON3?!nWF4skfX%Nm2tmWcVmARdqgM41 zj3`?yb7EgpllNt5f^dj6fPBp1iq$Rd*kCQ`x%XH(R2h1$)(LqXftG}C&e4DM^Gr}T zeM#&S$bO_VZfbW$%eOfmKSd0?pG#}Lbtl*uXukb20!afs)m;+T#7+J*s?5(Fd5>Sl zlB}R!uBD5IUxr+whRoGS9Nyo(ORwIROVP?Z_qh$P1v_@pO6>R3R5L8FOGTJ+Tw!oStu0!=EBE zv$Y~S8XvCgWz++kscf~}er6^qMrVZvL}n*_K3Wid$&78qBwy_fhs=DD#^s7m#)ZSJ zp=8--ENgm*Ks$;0L=hIvEWc4x4==>iHTtH*M0Ci7a5O)Fo)+*f;(AVA**vx%w>s+2 z81zo0F4zkPj5oF8Ow0wIoM6_W@rfVUIwxAleeVADD=^pZ;YnAWncW0>G>z*$KOg@& zdc6>|rgN@>a1!vVm4&3}m%P(G;|#5KT%IHG{>d2}bsVk%9InSpL!S^v0AplFD{Ibx zT@rJ8EbHHlfr^FJ%p=i(5sNil&}CHg`~6AU2D(yuA_m&{v)_*4o(h??Qnj@;Y>6P) zj{A$TlpUSs$bI-wgD$e`ANYy}+PEdWt;+euE}O`mdK}JLZ5Au|7_*-GBI(2(&Do9T z=}$^V`MG?|YpsL}5c)4n0u(_BdLl*ka(E752LKMJEM7W61yAy*3Vj@m5sZN10P$_N zSMmg~5n0xJkADZPIU{wWs(I*93CQ;|&V}5C%Xu(UlKFhF1oc}uXNn}JcNf8D5%9XH z^l%IGC$;6@!?&y12cJCpnQ9Upws?D7d}Yb%;%;jLzOP>)P~2Vu=T8J=C?2@JC0BbL zb=%9o-lM%5+;x7WFZc%mkpNBrr_pXRC4!4lZ!0^WQr~^P1&~GR`y^VUzk5hLp8Z1b zZ85}8?dZrda_CqMt2H3n)Tb`|Yzd1ZYz~9vZZ#kRp_9@qlT$V-0_}Kse0AE&_-NS6 z_~xd#?Ukc*?<`C1_LZgHzt)4lR9PApZy;k+vgHij7q4E~KI%GOGxA4L%l_|=Reu!2 zk@8e?>ta|8n_M>}K_sRQBz3`>(vQ%~HO#5Q%Yr@1L|-QWX{5q7e_jbO(9t(mM+=Ds z%y%0uJT*cmTi96N9S-(;JgewLgcgP`-RfV1&ERH*p}X z#)jXhI6!WObEQIGd#knb<3V}RxAZhUooa;b8N{(GS3iAm!8o|TVgHrsTuK3lgzdh? zpkz7$E+*H}*l0{@u?QnCM4AiBxRci^-_W+U1GO$!DhKk%W?95u=4P$B_~qGWR|oh0 zD*@V)i+z;C_=iE;@)!9TbZ=}?Oskb>-$&9S<4l%QeXPN4Dp>2cdd66PK#)y|zp80_ zcM^&Cf0KXo0|dRO-?z_JLa)TNh3BzrOzRsye--N#(OY)AbG|=yZ96Z`XBn?qQTBxZ z;qO;}i5~7ck{B3xG3T^fid++je{mml+pk8FZxis?0e%X=zyAgUeOI7}tc$4#T{e*%APo=O-vVXQQ@xDcD;QO zW&Jkv(+9mCG&Ys=!HGWc*2>WI91uQA%zM9?4gOa&|Kcjxpn&7-2s-nDuF5J_DUh>bY!i*$j=LBiYL zO+-l~9|N!9hceusdMA!!KLHYpVy> z86JG+P2{B93Qc~_2_=k@i+jfooe2j?f~G!P2_`!`n7Kx$g-8r?oj zyoI}ll8NfJ(1A{kIO1!1vZ%NXtBhDz4L<^^UK^mkRP)AzbU|exl04`QHqPHqB|x;2 zIDibVkH+yXwJrpdy%yEyko{^Xn2nE^*uO!5xE~mmu;J0;AQBl*K&P4`Y6e?F?q8^m z$r_Vh*?7r%iw^D=(jHl)1cU3!T^LZ+tu8^+(@XeCej*Q$>Z zh*Y>NsyU;h8~koep%Q2>lOs#06P&`7Y)hvH%8&y^=9!$>1_J7w$_{&B>7NA9$+ALL zR@R^W{i@?^KOZ+clkXHJBvNLH66N#8-F^t}$CHx@?y~=znpZgx&$mj0nV@W=s3@m_ z86AHEWB$%VsF<~$Pb&p^Ew4b*%oc;{@$$e}j_byxOvcw}VdSOHmuXMF!`Z4o#}rd%A(k@2B(!0(>lz73k2>(0D?J2~z(zNDl_dpF7-uI8`M*^Jwzu zr55++uYCh&O`c__V`a;A$pY#mT%KLk-=896CcLc}!T?VgbMrlg@MJ!VX zbr<(-)I&oa#vPjB83K`xk)@eXE1glWzPk5?5>7fD-fg)GVWYj>2{>>ifdWys z$S@G|CiXk9Yvv9I8G)T^ao+80x1{=?6O5TmLA2EWa5Ye$cW z{dT1Nsoj&9-|_7S#*`}4hK-3WauL59gP0T?YScbV0XIwe7%&(4qcL^CjIE3agIt3^X;3}|r!@X(1%@Su5*K;>K%khaDclw{>5MBW^h$5^#=Y|TUR zB!o>v3tbt4I!;M0=%b>(KHH*Hl07lJ(Kp0DA1V4P{y=wlnAy3_J4aR5wo(B`% zsZyGs-#-sPB`gg8Ui$XE(?{t^2Pnd15%J3gZa6zN{-h04(f+86@)Y40+@e>J9kNst zZzgN0QuJs4FA-_ONZuaCC&PYh&;H^O;BqZRb=SfISzt`63aIYz+VA>v!-8_<+|=PU z5ewiw=*{2u1PcId`yhsQ4Ft_dYmjb}KF5KZs4za1!bstFz!Vlap{pkWgGH3zQD@hu z(m=y_YwSthG^L}PqXm0Bk zm|;>*v}1l{Gqwbjs@a(kPmx(DrFe9~d#FLCPlpU}ttVPBmW&oG@#ngwOp8Ty&A(Es zbkecWLM-Rd>wlYcXTlDD!YD9hMTLXMkuzP@Ow7Hcd|TV=Q2ZtI%Kho{>y8{n#Z5i?B4P@s((7waAfHR-+V$6cVAB6|7`HpN zUaTu8CFWe&cuxuFV?&2ka|`w=k$vz7u0qgCeC$#>B>ukjYB1g^F5Vv zmlFB`^e5EtjYaWJQ{3bcU>NKKq`oJP*S{qK`coeCWEcMbfB8lNw0zw!G^k)pJ-w37 z{;v+{pO9g(06U02w{w%4WCKo5YWQALS*{ESZYE#vj3YP>Fl9jBJ+VcDD70>nTB2X} z@3{+`kB{`NemR?+5Rr$E%%O@6BL|;p{A$dujfl{bQM)=J&48YIQu%$IV-<5gbm={3 zuR6+w=LvGs!1(lUwaP zjKxi!D<yjgB$w{^{v2ELf`xXyOp+=eLGMP#L|fJaj474^L_|cQ<&!O9 zjO;~JE{9kZ<;pL%Ce4>+1C+c3or*~(&gBwb#sXgO zuM7py?xg_gZ6tJbMFss!ZSK&L&&MUz@B2 zLhgq(dF8K#OmXzw+ zhdI8bMP~Yk`bS$pYu=;?Q4V+|VUoK9I~^rx+!sy8X7r>VDZ>BRGLvagTB}BdKt{Ri z4=xR^0`JISiMv>G4OK&F&53}J@6xn=GtUoiMXWu6Hhp=TbqP*C&hhlf94lhq!}9`L2tAu2L;OxIWd>mc+h{%^$q9>C?$4!={3b2 zW$vZ;f0JgYJxJk39mDorCZjuQirUM`(3Ij=y!g-MznsU2rf5SI-# zE_I;f*ECQ;YI@@*?4n)EBzIZS1Ybzc+IkBaTKP9%S(1xA}=hi zMySNii999`GHHp`%&RC51t!{m#8HP55ur0`d7wPxOA5H4ql03#gX#h13g%2;0Ol?S zbKmZ?a`xrV8($;g-t;}c(UW=qyA9NbziOHopAd%U#<%zOXqcFgp_`gIo&N3H`Ct&5 z2J&FZ3}Gje#55>EmOdpVS%U+E7eB*7!_8h>bB@Ys(N$VjbzRgmi+1}=5f~}Q0?=O+ zx?2DasG0RI+RVReV4~J8t_?P*`kebM`BR;W{yjG)* z4Dfyb6J;o2oERUE%b+Z~Jxv@Q89>OXQA91F8v*mvj=3E4OYvoRY-FVG>Hc!W<^1t! zoqIE025Id6;jz6#jK+e@-seMb!fln@UbcwUBl4qE|9)c<7~~s*lK^FmpQ)R~W5-9x zpKco2AS~wiFgCrVrC#vAs0-zaLI$!>U9#iXjG_DnCxD(P6@eXu3UU<91O=@-#9H)~ z{rj9-)y#C{sw>A@A8JNu3uqXcv#qS*wEjDBUlPIf^}MnIC!)I1!Q%OZVzRL)DdV8qKkp1%rWJYuvc$vI{5TLf6U{jHZYgumoa5sYw~rQTbz0q78$Pea z{`w{D^4#FCRF+%z^Jfo~mJ|;L2NH8zQt|T6#DmqbByld#6@e_6X_?&AT;4dFI7M5d zhJ2T0IDw4o}gj-er@2P^2T`9^(ew&x%}*SuQ!970=wA1^NI zMbaW$K>J^7I2*J87M9Wt*s8s0a(iL@sB$aj$ECh~IG9RO)&cGOy?qoOGJQl(QkVb+ z(@h)>4;d~DvXS@0Nzzm>k{ebU;N8O=oXOu^&`rP8F-z~Jxv2hrS+IU%R6LI#$xqv# z6H9x2*ZTaNobEWbws!ATa>Qb=X=K--Hyd|}`T6-XN>&jN5U8MARZ`IH6QG9&zpb5J z{YwGV@IDD(ypMd?X#odZ)cK~2)M?>p_z`@tg1lnb*jxEv56(3OB?b!yWWiyagabJR z2!rYpxQ#gm8AwAFVDEfMwnMW9h;r!AP-$+YPMB)QD!EaRMEM6n3SH16cEp+dXkA7|LhiLX)9$c={BA~= z=9r#m)YaP&!~D&;FpXU8yk(AZiLep_0PMZQr!Ww*MV|R`Isp|jNtQwo+ z)ErUx!iWh!vWf|Nz{j-)S^4KqA$pF7`rRMQdu>P^jSL22vTCWO2lx}g9(Z*8W*#!4 z2RPuwBlmkJS{D)bbwIWZG9>LQzau6Q&B27zsgjNjR4PZx6ZC;Psd(E<$|noOTeht&I{8) z>5_z&Hs?IM{jp5iHCu2kDn3?z%8H}m83K1)QfgBF)36=X+y5*LqY)zO zx>w?5Q&rtgE89btDfLHLeZ3#j)neMM2ToW5`eP4Ks~H057S%A>g|Mz3VJLM*$NF-{ z;vyrt8nRG2IFQpWKVs~@n6UNbl|awBWk{Q8Xe8>kR##MvMz?!i7A@EpwY8BIG&K=H z&mef7t;1Gw<2X;`$smQ2&X5T@zu8t9G*poLtzH|uJ%7G6J~{h#bE?iBfP7z+2-c$K z+DXk!`2CBFjs_uFelmuFwA*y)N?6!}J7U02VC38FCoXjy=Tth{oPd2VKfcJ{#v-;C@6RW0K%3y`e5(Qs;kKYA|>PKN;Fmi@Ol!Y-^bgF%i0F-(eWR zh<-7%+Q_N8|1Dzfe~GQ97Un2Y{8WpB1Iq$XGdl4u0nvtrhNcR-&{NaUjMUj~o_jWx z@_JPBB{&Tx&@Gc^v|5r*Kq1ujO4%aBF1y3sP|o;zm-G%Q-S+58Dey!x;>cM~6ik^J85s1=Eg205ycnd1BoEPM`z@217Tlp>IUd|C#dUq@Xc|D|YM7=RKKXb8a@4q9b<)y>-UvsA%JfJmikmy&iL^ORHa4w^Pi0{6mgd=F;6}QFA!7S8vy8aKL-$5%^@)qdN2xLK z^pOagzjHq5lII1r43OOHd^Lpg@qD%EnMcH|Gj?=wWkQ}bEw%bQ4HtPI59jtvUGK%N{HgxjaowOoTX6c%q6Bn)4T&NE;A6VI zt3=$=aQCpDKEaHehs;}>^*h*F&K6ni*`I7Au$oiyTQ)n1=2wy~vD&!?T#4>dvN6kE z=wo65T4TVGT5Og!Gx;jO{QVyj*YoGpy2$ql)Z67tly*?=3x7-E)V}(q@uMcT?l(Q& z5(n`%H~l5-f2{%k&5a&u2*%p~Ve75K>So&QVYF;$ad$Qj#hv0CcPUbewMenz?zVAv zmli0c6nA&G;_gt2yW1C@KIi?;d#+z5e~^8JWRl#I$*fsx-IIs~=r&k>EC|dSI)7L= z<>%Vn9}m82){l4AVHA#COpqx<_Z^^`ace<#-(YeeVye9+;ASNN>)1?qo00x!H2o-o85ds#3K*znG8AU!)t@>3@Fdt3ojncR~CI-Jt#l(f*4P zNXYnt=8AJ3TEWGb3XV2{2OinM z(UA^-d+9*cKTn*kBNab9%IQJx3*MJ^-p2OZVf@ATpP%nni9P_^#Q+P~+S=yG0{ff2 zwcYr4|4GKa+>Hj=r7Hj!0T$q1^M_z^geY!lPt`mva+j)?-H#}7>6dSQs#J;S8N|0L zDx1hq4J#FPYKktgADIPFO-Cd{~8pylx*YyqeGL{^apnJ)Ji- z!{yn4$$MV_CwPwcGYl}zwp-Gm&D^x}m?n>X1>84m%g_EN^Iabj|~bC zL!Kk2B5(QZWL@rF+v!)h`Winy&-6SaQaMsG$U~HSI$6bT{5ngEA?q6!)|GZT`ZL6} z9-);LYjX`+lSW3WAKEb3MWOrBle_#_L1L%ZA3JRMLhx4%fO&uYD}(5TMo|BXDR9W= zM@w=H+F9N#-kd%-*$opC5G=DV{suF5?XH-t8WRz1<7|jmG1zeQYMG>gcla4^dj=C9 zAB7afp~m^Ifnz+wLqOh02IXnZTu`B@>-OEzPu-&ViAZimM*~kd&PD*+_d4I9yF47e z$_4Rrz4;g(O=j+~w1U}88+U$v?RDY4WVNk7nsnDCa#@_&*(qK-E**WsT!q&#oEfOm zOZid>3jXopmZ(+Fam?fs!{Ez*P|fEk8dZkgdu@d>i#$-Fgw2T5nTyTY3hJe`GILa@b&np1l5<=w0B@`ReLEUPYh0{2zoW3Dii@jp#cs-%IH zAF9P%)rJe-4bpNf{wO4VA8SHY95S4N(_o-`)gDPXRu;nUOoB*<@V40C8W`1ocI|L3-Yg~`m! z+@I!Cq3%a<)a(|Z=&lMbuBO5!DRO$KBp>4F1>5!6yLe?fRds4-CXjdzth zRtC?1gjtJ6ZLsx7*%^A0teN3-M@CmDBgPKB*L+|Kl6r+JPc|%#873^rcWKOvsiw?A z6awgSLdU0YR6wrg?9ZQ^u%yt1^-| z-wy{XySMMvss2y3yH3M#^E8vd)S^7Bsh$a;eVgNPBn5Q1NT^7=Fl0fLDdAn}A)i0E z$mL@-G&IBvZc*1%6~FL>{}it)zf${|X8{Xr#X}5CR$zr9SX2{17Y>3}j)K3dGxlwt zVE`uGf&CxYzK^itpz!fQ6_dCnY~kTbiq6lqa?TL3ED@XIjbNxwMr^Mgfz^Gos>y>xH3;NVD< zTf_I*e$>#Y?ml_iTt2)AL?1s!P1EO|#F7MgLQ^`~j;e!SFovcH_0`Qp)WH>M=PIZ! zgDEILloml>&CS14gywy{sr%&bmU%Er6v=%_wvALl4*hZ_(6hepH6rYBREfvbbDMFL zfo(@JYHDI4>*~sPhgTKjVQ>EjM{KU1pM%S`j_&-{*sJ*!0Rct2@PmYP_0sg5!UY?U zRB*s=etFsYrJxY~_wMRLon?q7`fJ~P_l+s`&EMWQkRN|pj)m$dU|a|z^wkNM=+e)* zu~T`E^!+)VNO25NLkilExTk(cRnKwFRdA z_x_CK<^$y2%9P)A5ygZYA3G3IIZVR(1wsLay7A-v%{pGTQP+1mM3YaZvRzd&Jj@{y z7p6+`i*YoxBmyVRQ{&DBdZyeObfSGDPSf?9@xd2jqQ$>|f7+QS7|j+EF?h#@F9@Q` zi_$^@^bN7g%^pSA|Hac(1Lv_qGkA>NAet-KrG@>F6Q%a;!ufU*7~|u)hD7iA^l&BM zALEP(%v)MPeqd6TZUE`Ayzu7FxtRq7bnYJ>kf%No)8(0dX`!JC82#jHq@l@u#S1w zWe(k)KmskFrZ>~|4%@cv|EI2}s(~>~`sk!@j1SqVsU>I?($obuJ^*MEi-IkXP(+Mev!lV{vz z(`)n3{pQ5S!Rb3s_qHnk zPAVCQwb}L*R2iAIF>C#FffhZ4D)_$qh`%!F%rc6xc$lgCgl zscEM2e~0wGz0%K4>8OF3;N8trWvy;O7ks2)yi_iJ_1{C0?BN$-147@xU6xiZ-rPs5 z3mOdFG$z6?=b|z+eP!(V;`YWyM@RXRoDUae4N~Q1;^~xM1i?}FKY^fCFDL9adQ=m7 z9Q1!!Z6tNkgichHmHk?NSyIurGeP~~mnb3eJx)EDHct8#kl}RA@m&4*_bp(nzMb~l z6R);fqt<%3_3}MobgzjitlGKUw`K4twV*`%&yy_w?)2brJl&Uu$Bg zn}66V);5&)@AQ@5u@vr*_wVyN)lJB(B6^9D?+zp_e4kx9xSqyHT4TSQASCUd;?jzUam z>TwMgA~x@_T;~Lj@L#@ft99Mk`*iZC$Ar!{Pf`Yc3;}33+d(6kp;tGAAc?xo6+WsOIOZJ)E|3A!P6tDoPXw4EXzw7n)eF0Y)XRV|7 z;#B^))I>U7n`(I}%@XA5md7)zISp;*EsalO{cuxEms!ZEl;7*}bO#1r>nio#>4-Mo z1|LMhesgKez0yWXJTjbH4iJj6G5`pvO1nS5xhGIim!Y&FF;UV4JP3$smFRPiG_tzj zq+A{R_#V^p7(G}`%!^79mcpZ_*?duxicge2!hdWF-CuGMRk^P~AQW`0Be}bTy4l#7 zBbUgJ>}-6stha5i+g|qORmH&FgEbpmd1oHg>{>tN7cHJwRySME(pL5U1;VR!d0cW( zJ9N=#mHGq|FCK&*{}$DB=+Wazi4wnr|Mza>-nhsrvMJL`$)R_e z0~_#gBz&%eo^w^y-l=}k7X`$BjDCBZ-A?VjK)B-?cUJio?$CNWPbvGZ+}|pvfN&7E zXnFOj@BD5t1>bn?6yfYPl9<;xgfr>p=7z^Joi*SZD~uiViUf>=Mp9+1dAN7bHC4ws zTvy_G#tffEJiB3ZxL+s!S*iIS=D%W2hWsCB^Dp>)QSAp#6Qqb54@3_QwogvP@O?aQ*pKTW=_jP0*wz9L%X~{s^Zi|M%A2O($p4` zw#`vv+FvvDnP`Ctg6g~2z>E#^i~FUI*&19Tr0?!UY_(HxenECN@)t!}J-Q7#r8ah? zuCFH~hm!E{!7tE|s0nKqlOhh(Jg!6F)7{s+^9Zkbjo;-k>8G{bARjqpxIUfub|SL2 zQdA7MxC7HqB3j_shGHPWmiJ=s0mY)T7~;f8Lh7}f>mGBy9c(d2*!0`T=S z(ZHiznaRV1+7%}gqcmO{bf+iZMyb6XXtmShX#4lZw>l@IscEd5&7Tw%Yne^-(#*-N zu4n61NdV1LAsHDcGfT(cW-HK1(v^SMkEZKGyh4ggou< z7w;Dhqp|JB6Z%i*@Y4_YVIsbl7b4ptx>ogK9m)YyK}dy)is}zSXJKKXWvVOE>ZQ$_k6L?qW=OzF9M{a}&Cr(Q zmFARKp%^7~`|C1&a0iXL2l0kcbTt20nW@QHQn59tzaEt65@qnhY}2I!W|>fRls!vS zW|ci>Jc{N&_XnDpVr5Sg^LXcIhA`p#U!$XEUjrO_e^9=|b_N)h`~DOns#U zCblP8ndsO5A34vz0gnGt0ngGoy~jWDcbui&u6bW|iUwVt+(nF-jLszVcnLE%%du?~ ze?MIp(3GLaWi$yoyt;jyB&>Ja<2eIKD)h}Dg>dyg{aRdaE0D*7ppghlM3LQ1i~jWQ zAmuyhBmC2Xo>u%_VOjL_mD-8-^uF1p8>!1TzjwgLIv!KjbxYRXt*DGW1WQU`jHHde zTeAmId9?|K^FyxRhxYb%=o1kE9x?H2Fc=({k}^95rXEojK!0+4Ali_`WSKyp;^wX+ zN3PtiRGaPnOx2OwkDr>EZu3OPXM{XdIiisfxI*$2bwa7I0EIlcmM3~we@HtE3JQb` zI(R}{rA37$C*Qkt@ugRZ^Dt)WKQdqT*cqp>x8p!NZ|z6sw_BI!9~yEL>nA}fYYBN9 zZ|f7Cdr|DNj3{BR{6e9osw#vEwHv^V{v)U>#HctkIiriKS~(+8)U^iVWwln-hL-Nb zse^$#YAYsVFXnzK5r>a;EOUaLRj_-Xf&W{rE$Eki8x`Wm6wdFO5gsE!mukPl79&94 zt3ThML=}WYoXvA?zHn?jcdhb`_@z(c1l4sQrNDy;_sFp`sN+EHd|@pmA`*Oe>=(0- zt*Ow*18#IM`c3#QFlB{40+Rojmq#stu2!DCJuooP@CKTfR`2nOkS>dAGwl)K5=(Q` z6_#{I`X%~KXR7L0o8&FhJIh`yvnJ0iMjg`hug2=VFCI@CUbE7ds6pEM*+XBCzxm`= zTzJEKc}OQ6N`caLozGUMjy=)_+f~d?>%WE~74j$UZHSbzs9^*CPd?w*c-UH>44=Gi z*_zZ9UB1!2V(T8dqQa%k%=-34Am(OV;bF4W%trh&-cA>0FD_RfEd-vU;)Hr_|Nbn10sV#a z>?)5}Up>aaM-leR7ar)tuXyDfcw{LRH2yN(Aoo`l{0vDxab=3{?<$F#F`DE_`@EEk zb{FxZoAglOcZSw~4paWRnYdCMfch5tDf&$M5xwt&c( zpx09)`+WaFg5yALf{8v<5IBiGj{*zg{b#z>GhY78l0pcHp;-UB=kv+Xg&@=euV1W*1Rad8CG9W8SBjycneZ4ALzHfJrsG;!UPWX_x`winf~}u?V-Q&Df~4h zb(!gA`Q4O-Q^LWv8;jgZa>>A;TG)>Y-+XrUW;cO2m-wrO#gBWlUfKy(3sYOew$q*l zRa;gSO!3`@;DGkF1u!kUAlNTyqUrpHYNySXfg&WB|P?gNb`{ZVahtQ(1uh%Pv zD7YxlV+=Q(JZ6ltAZg_2ZwYvE1?Vr|g5n^u3jhgwo+1bVpaA%*iZXb%NvsxOa$y;Y zCa7WQCC=RGTJ?2Cr2)37#<&k*XfGqVzql}VBB)0a8 zCYNDo)r2+=g&IYkOjR#Mp7p{DdUjVOEzYwsFHSxX_QYGFYaASXv7Kg*Jva(fDg$q! z!-c@e8vD;Lo9jjwwHdyPY&hrc@>(N<;o;$n-7I5}1YB#Jb}~q=GPm*Jbxtkcmc69A zzNj@aDJJFHJUJ6F_`|5=ky0!)rUU8or6{4I*a_t+P z@JctdlabWV#(K?xz2TxtCsy}}a*SZfuRS-}RI4Am-MP}yj4%YGq{Dl27m084ygWTWOifPWEG{jL&CY%Y$F_eERa8U;tUhAW z(gw~LJ32eZmY1WhuU%a|Jg8V$l&Vdq&d<3xI5{zKaG)tE1d~%!b7Fm|2BxuAT$I#Y zIFCt9+G2He=dV`KQ9C*Zv5Sg|cHdxQW82!VcZ5e`UG4yH=pkfrTag9rMcK7OL+&}Kmwrlj-593Sma)P08rjHbb$+H}U0?{0 z$lOUbBK~9Of)DYLw6osMdNYg1D+l9-%~owLJE zoxzo=0JUMP1$rwuSv}d5j5PoCy_vFmPmK!!x+@NGF-{u;^7D%eKMOOnwsNS^y@=dW zO=&5sl$4Z;sw!;m*!NqpG2Y+56>s=nV@4ib|iGc%CCdF z<#W_ zF>P>Ob>y#KDwn}`L^*&n&&j)k#)bw%Z|~(>FHOZ=Iho6&3;4t9gR6ocB9po0y|!29 zDlq$MN;N-P@<6Qh^ikXe00!KlP}6^g5_EZfK6|^Z&slS+J@-Hh zz-}gNCuM{{`qq64>N7(` zqchXu>)>Y2r^wxg_3KtOnSHtMvSzO@YQsJhezic&3z16%E8Tq2Q2$OLipWUG&Cq3q z(#7PLQq1_l%#sQ*9u8R^&TlfN%#HUXj5oq60<{MtdYd}?^-^w;+bbiZbP7I$J@e(J zgJqYa^w{4UNBq6Ps=Pb~@#i+%;a5LJva@KHYTdach-avk`i##sfK+lKt|y4~ODpIR zlfr9BLiU?|w!a4-VU0La6=>u8jX6Mo5}RVPPVTHOG=aB9-c3GQ*Ls&^N^N?QIZ<(j z29Ku=%ZI{v?8ncQ@+$P#S|we`Tp1g^xU+%QR1`Qd-QiAQ_O4g61R(l=jUjKSh(MsZ zuNPlCM4EXN_sg(sW=wc8OM7*X^!tm6 z9P0Gny$gv)Dq7DMM3Q)>~+ia0H*+30b}1pmra4esFo6RY=po> zRkaAuj_?cKacX)-w*B#;1Rap?!0?b{99G(&|52T*GLPSjpXraMd(KzLVKRV|v*tRA z->na_iW2SnReyUJB#11rGHHeK{Xy2TW%260rRD3jss2ay+_Ua?j!sVf0M*P+o#A`K7sh(r(7EiS|@a8&~p!bFpY zZ<>0jRRLB}fiaz!Yx%y3K!U6~6Cl?iQ3L-AHBg{YZ}U z$c|uzIdL(G7SS991W3h5yp&R(%XBencqkv;rfGJz28C>hGhD{D@FcSALWr~WeS^yi zCY-`ZFXBr)TPErl`5dr>-WkyCuU-vfQ9>c}o;hq_r z{t~dzyp?iHKUWZrwd~Vi&{mD_y(R}q_pr7f} zCHJZk2OagOf^o}iuP5gE%EZj;5Y1>G!0BS&_^g`;hEFtp8~6J9`LJJS0IVpKW6SxLKAWCkg4O z4%aUL$Nvi|N#v&Od7!-SIZ78ml5?i_hggfDlhos_lHyZR01Az;FwoP~_RUsA;qFgy zUnbE+txuo6ZT|k<;7`k`_8&1L%h{y`1bR^NDYEsJ9Y#CCY?^ zgl)1UaS^y~_)8Cz^C@qf7>#;`OJY3R1#!>+9}&V;mkx9Mq=K3QEQBX3%}SkFs; z(zew%x?CE3D6egzZEN)?g#gC=ad0~gdr_IK+-WX326t3*+D+-}fYLX>_!=|=p8*R` znm{k8vWY^{blus1I!HL-$A7zVFQrv*|FPXO^?rlAye_Q8tUo(xU?8JaB{k6>tz>+E z*$Z)f>tlAX#AamffkZiZnY0LqvM}9Z z^Ohp-M^#nAVtCygi3}q>y-FX{`Dhsx27wDMxer!G#u5R7)ica5N|n)tQ@54|m3_-h z==MDkhJxxwedeb&{5^Gq=9O}`aApT583ycf-&Mgzm#P0YS$Xl=d+fDIPkkYRTnaRd zu)8kQUay@CKa)k`Z0Qyo&4&x-Wc2ayuRL{GJMl;GF_wT}2}bM#0iqF%Cm$2+dmOJZ z4|!Oyw2%Rl*|vzw$s8>{)e4nokT|IDhV$)E^6&9iVB&>TqvmY7FOn1e_MF3y{kO69Q{+9j*JyF>YJ(f{ zPG3nUNQ>8>xJ|O6SZt>y`}#uh0ph(a3(NEb?v;d>)d%A)ytH4A?=V@+><1{yE}Bh={{FLr}1@!CSi>S zpM@UFGdID8z}&+K^g9JNg3LnUw!ts&%ydXY_N6nB5R7%_E~j^=IQC{L`J?<%&=l}| zTxEWRDvQM8>$}UQ>F88(?57vmSApPj*kB=8^D-w$2HJRiD4tW4=o;MIZ&>&`Kn#+W zk4b|Ug!qT(t~(<)HmYBYo8Khw)&Hn$^?7izyD}@uDZqum8B*RTYDa5EM3+>4$EtyK zQ-k73w2owrG77#d?pYJQ=gm`Y=|cRz;MT2;Q^)d96lAoK(ZSvS8WF#4nphqWp6sFAS9%`d zaqSI)Ooj47+tl^iS|^Nz%kc7o*CG;I(m^N&R&oJ3)d%WrdRGKtC`RCh{77~#sY_-q zxzDE9ORp^g0FO_*Rn)}ehw<_tF0;<{In05t>lbXSYh2;yVSUVrwVkGnry9%pH?Ua(uzHaTk2-Y=8zRaKa)14y*qpsQQ{4`K zFcK7=ZyCA!o}K-tdT7qM`4ZM(ox5-VYmTKlJ2@NXnn}YeCi)?I);3H8WjBEVnP#_ z)S;kR5T7M@BYd>j{93+`W%J zc98X+Ekba;Nb#9X_z%j2E<nsk%~JXiyha&44cwrLf{wU zD-LaB?^E1qCs&ee10RnDyWef}vC102hLQ=xVK`;#CKWlXe9PYK%KNT}0I^rNi$p6y z-A5o9XYq|Ss`lr-q0N@}SQeK-v0jbps_qvu8pDKBJEjK#GlIJ^&P zZj=w@2;1OmX|f=Utq!q(Un8@?Mod!(h+;ikk9fk$ zJ<{Chb})#^VaFakwpnkw;H0Ti^P15?Xh0W<18H7RN3S?h8oly8=Zd_eqUZ3BF+peR z^c5}tKiyNQb3|G3)n^%>d-1?FlF-k=|7s=yA!i=w#z(48=8uf8oWy4)O`J^iDLV}> z;o@7@cNc4xQw8xqSXX|anVLEY3Jim350W;z!&>(gDipESh-MDcl?o^iraCS30Du#a zTQ(_Ta=d;p#KdBaq^z9#85I@9aq1)mq(n2a^Xf!UjD6ND0!|PAC^GvD=&l=cRhxG% zcBgLl-0N%fEi;v@92=&V7Jn>{=v!UZSk{VsUwh1x+6dT1mp__4?^n|LpRt zT?Y`P+CHPgfHYO@*}vhL4gAm6I5oP2OxqpyzoOE#&D71cBLgVHk@7Rt4QO2RWXVEe zRv-GGcM+MbZp@-D;fvZ}eUY!W0%adts9|lRg}56naGhvM%ExR2+Ne-7a{9uWGQlC1 z_w^Q3U-KIr1NCqjYQ!Y+CwKijMn*Dh=;#Ki1wO*jUie?%Q9liR+#cFDW|g^2*MO=6 zPB#xQ=o1XkF7}gOAOM6R7=hcaEX7{ zXWC>~%yZd;9S=^Fv=_NL)7fR>jnB-BFF3g87Vg_!tjnx7KfKa@42pCDDy&q^`C_v!U$=Z!yL!)!?o5u1atmUs%iBrT;6CWp<3|h zBHJ!?Jat?bly4u>32TV zNS=@TSp4tnSBH9LqQqoxOYK_PD!$)upEBynpZC01 zEHxY+g@OCOWP4aAIlNn<_L`Y>of?kM(Ayl^c~=vARC=72P&bl;8D2(9uJp1VoCR=; zeo^_LcW)|JC*m%A`$~T-yo>zq{oI>b%izAs*%`yYt0=GRKWAMcwavn(4SK<&*b3Kk z2zx1IxWtY74@SbB6%Mginr-S(oPUDs-zZ~dL+l%kRdTXhFgNWnXGy1koVuPfS=Ni` z|0W^Re+WvIKj4v|eUE1UWsPEm*Hcu>3e~GXB(S<0XkVU2bSzaQe71m?l9AEnV7<7u zXjZj8euUGM=Ot(Pb^r)Jh_lzu@ao%yIe<>^IsW+-G-eNQ;nX!X=kL0EJ5{)pdiJBy zUDhm5*trCI5PB*sDLeWIsN4a;b#z3;F`%lvxO$GO|E@7dQB13{`?b7%Ki;>uk+ZTq z4s_MRk&E@SkV8Y_xiw9WQ0&bNcX#5IZME%>-ewOWM`^V)PM;Z{hdqcJ6b<$W1EX4%0YIZ@#?)+&G3 z;Q%;Ie)RSxn{hg*U;Pd$1Tp;OE&pwTI*~D)izJ5>dt7$B+RAJ^!k>agI+ZH?cCzQ| zJnDx_3SR8hk&@i$6cO%U$9!Q$b7h$vSg&()Agx`Ru79p11piCT2!PR7tD`Bag;{QA>1u3|l?Xgv^i+(2{C3-1CMVlFd`)6-L?@*b`Y zz^=GnclK*jQ^wbyty>*4C3^6IOWkt!8}UAAC*&eKU9-`PwvC0ztxp>s2{Cv8CaalE z0!&4PV<{(jyp>pPbcvR)6&DvD1BlsKS!CBNI$_6spx|w{^Ey(zLi6C`lNGuYc9C>W zq}ValgiL$%)@XY>b`N8B*OA_&W47~_Yvh%smhOhru$Q07F>pfAdn0;kBY~4GlQqKo z5OWa7-o?#nW(8o?{_=Ew9Ah2~IW`dy5r9Eit|ul2i|tm5GOSUg3FhZd zoq?B;IsAv&A>X13(f|2l{ar9xf$B9k_X_7=xm9WE-R@LTY(hd;OL}^GD#m9E3l4{V z!8AmhY)K5hAkk2>u|@k8Iqiy-#;f#`g4TONJrPG8K!uydf0s{#$B=Wl*f`kCu^{l~ zT5t6pQe*w`F48Z(jGuZ|riduEh{J}(20vm`(EAjsF?vN*Ez|=VnGQyGeY!%QcV%8r z9JTft0cJMeGbkp;FoJRaCq8kWw=^Iv@W?lL%8(=pRme1MQ0_N+6gy%-8rRDxQF4ug zh-|5n)r`2MosQXfYD8_75v8(na&7oTtNS_v%dcKs!i40PT`(3F&w2QRUZ1@|SZDA- zG<^p(vJ7UeSvh)OP6$6)7Lm+*`?Z%+Lfl2eH9jRGJb<`k|X^4nJHPReu&F2!DY=6 z?Ok8;h!_xku9nH^Q>ekRYxKvBZH~XoVyS-K9N(4(AhBoetiK8R>bx18TlOCM^L)^) zx95PPAY^8Grd+?J^4*ZZsw^_Y3O|ypl-i|S_C3k{BSx4(BZ-Ls;7NBmlFqA4lP4$h zpW7w*yp8 zZ^+L`zMOp4C@xZ9VRx$(heXG@3H%pVZY-?(yJMlj z6`B9%JrRXCFN$iMD5-B-Ne-8*=rTk|X`W;K%oKQ4?mdBdapU;&gdoy}qxLJ4z^h;? zd@%b59!f36P9GjtiHmYuxxQV0A<;W#70Bh&cbq`Di3mZnZsfVM802U z?>wQN?E;7=qpljOpYg&e?>>J4A>1uxrkHIe?thW}$e7}|NfPar1l_!H*d9#r{#jB# zu>E-A7X6SmSSHzIALVR3susKDVa zAI>PXHhW9E3LhT%>?lC7uW6}VW>838Y5PX>(R?3@7JJNc;=}pPY}KKfK0ya2UxL$b zl7*;ErcRV+Qs-d?xFO!8@aLMN6$4_enl<_!|UQQl)@d)peQT$Mn;A| zG@VXip}$|?(LF%pY5plc4+2+^`xSBJyHKV`7Lf*-M zZo-N>&soOC#)`QKSTFMJiIP1yX(1Q>`*sie`W@&+3$ad<g^CzryJ*miS&ue2FwFR$Ns$j4G|zk_i2bpnpk(Y##7s1>QnG zcv7W9H7pi3KGEjEJM$6mQDiUQ0Ti-6qp`WAZgpcIoh(=k@0*c0QfU*11>f; zr31|?mrv+t+HZ5Q)c*ZS0=|`=pRN`d?BN&eqLPLeKEa@)l13K9`WF6aborr(2lvGX zk`}ynF)g~IcFFwTN{*^OR#EGayPeNb5X@Cgam$O<@U32=&1+h=e`CHm%bc+dDd&GD z`vv5;&EG^F{MnC5q=h8UN+gF7xp4C%ZN8 zEFtfh+*0y53j?TN^KWxy11G${K*O`uQy7)8lP3-T{OI)U`<9=jugF%6ug>gicnwS} zH?~$Eot%Z6hevu7J#`F77p@|&E}VH+OgwB`o9mMjI;k_MC7~IDe31!iQBNaz*s+v( zyw5sM0C~u`1FixW_)~Vu6s($G2J_v)gS@T=sTK=_x z{0C9_l<=|$n`+ZouK89j<6rB_f1s36S_n^~#Q**!{YM6-j|_Z)I?-MMkq}Giv(MK* za7==9U%$q1Xv{y3a{r(rn_VZlgalp||488fB21i=5dUYD^IwGLiOxs8@P{{A(b%PGp%GF6W8)K;s4*u_$_b$ z)@vI^Tmz0U2tePuSsp|R)Q96iA50_ zQ4Sjz+ZokLy4Aw)f2m^Z!E`FZs?yJ=@$Ar6H!!|uby}z@;r&4w{=UX?*FT3u#N&5A zTdG3HI8Wo3;qAnXjP_LiEPg3(*X^5;avO(nwPS&WOGAr_&#VOT#vWq?(aDkSW^osw zs4$4D48qe4Hai6C@8Fq=8Yp=f(J-!E)ZqMa=pU=bFQt~En`E3Fmr7PgRGv0M9YYr& z>!0Eu-jaTOlZMe)irn-2)BV_@uWmeY;E`3&Zia3b;?Bv_`8h)YYv}*TYC`~bOZ#ac zwVD~#dJ*xO)(UcI4@+IT&gw_=ZvPsuV{5B`l6u7Oh4(pgD=?$tipTraZLLMPJ4s4E zJxVdR24AaG=HVm`qF`=gqtTC|2T$*v43>nF^tK2EIIPAhwzG2H7{|3P{qZ53Hu2fP z`rj6kk1r@U>3v21M25^xBb43-q%2G#B~OdH&Q+kFF&iqx;1Rj#pv%>V+xu6B z14Wo`>j(+=24X9lU_4G;*21mG4Gk0s(R$UtN*jbaiz7Pcw^~KVaN6z9vghUHwG9uW zhT}0qG&S*Fym;}c3g-=wD&XPap_Lxevd@99jDyI@&c_!5j4#@OfiW2w85_Z?>9cJc zzW-w>nkvzUFa8$u*7|jmh5Ooj51g|rlJ33623re~cW*T#t9=-{gU)$RlD-Q*j z_qz+;>}kBCJUjllF7PLcsaCp=PjNP)1>4@(7~$~n@Xz644=_|<6BJ}p9mOLoczBfQ z-G+mQFGzU zNGQ`*^t}t(HijPKL|nZkQkG2(Ciw6M(K*`L;noj2d>Gmm(J|#&Uwr+y8@wMH6ht4d zH=-w4rqIyPdV0xyo-_AcUtd#ja$@t^EF(aLAOqaZPJ4Lv_VyS!I0yy?27s~Jg3%)2 zd=#6O^`f|y9M9aKHz@L^jEIP=#}nPf({_^$4i3&3%VBFEvCij7_}d1+JRB7UIJYY* z-elzDy+G=lFB3b)avVZJq?!POgZ}kvaSsmxeXk3RgyiJT-Jw)Y86~Bjj=+~&XPJC< z9b{xvtp>C#)$fZvdw`{IkFu8|efV9uVc=?A=%Yl;k-ZJ z8zXGM%>0;d7Rjt-@Ydh#>cc9sZ^3A5r6U9ar&+4cEMHbYOQwN!0f9h1a1j+_$8#`f zYiQ8O=Kz^SR!P9*!k+Zowrqy}6XfCru~l^<_3*zP1*n1&7Pa zZ+hkC=Jt(@NJ&a|S8eO)=`pW}7ru}{&WOr30;Wc%kLi0KM`sC2mg~@p0)&E(=l5FZ z85t|Gl0BmU9j~nDJ&6^2=PO$`T_t$psaPjaUxnrwEr7dxz zUbpAwmVJqqd4|5m|81!|M+l~a0TM}zIgTm);Ys~cWai6L3uv}2W6NpL#l<_Xjh|pY z_iKm$uC`FP%Ok&bCP>Pt9V2_|TT%vbgyMansli!@><-ByM-cROhB`(>0<(WaI5aJp zZ=5W4iTV>();fbfQhgbln!?p8(FGD&F6?D)L`t+aG`tB7w5W@h?UT3o|JZvApgOj7 zT@+_RaCdhC1eahT!7ahvgG+GNKnU(`fuJFHaG4N1XmEF!!QJgO$=YjW@3Z#X@0@yd z>)xtcR8fET?C#M$#y>{)Z{Js^Od(tKnwX@)xS2g-r@+bCr8hns8!0O^r)R{TJ6(oC zl^$dFnMy`(Jfrk%tQ~)gQCM^sEG!Mfr?e)$h>w+DW)GXj>ll@@YV8&i3oif|uA;ng z4k1k?C76Vr$uj-@3-{MoN~o%lec(&&q<<{`zfcv-a5sZWXI|1kaH)+{p`Dl=Fad;1 z%)Ufw8W#AziPEk-U$3`-QE7B??Yr7`N}2qq<^!EOsQ|Xj@JBWY&o{y|E3y?&OK`qB z;=^KNQ3f5QPvvy+hu8r0dHzXBNvDl?v@4g}PJDN@*hf9gT z);M!0!TbPV|JTyTMB?PV^%9ecv*aTiD-Kvx_NQ#Yr8V0$ll_ZE&W z_Q5w#e_zEPURpGMm%>^v@=PfOX-U~r>?j1~=gnAD&x=6pbYf=jpBade1gx%LV3I|Q zVZer>_=@Q|2+zdH44X1^W!Ay$=x7BLx(i4Js!+t6!ATYLN@=2%C_ItTc@!2NEiDF* zfi5YzytQGfuRw}}6^;!gE<4>|&It((pD$y2HuVk+BuqiL2pQrMh$1Aueze(oIk>8c zIdZtc7x@_P5l1D*y3Q@$_3G5&&@QGDbVv3ASx62R1to;4f}NXNHJ_^BGE*+)jD(oD zN-^})e$4ANwe4eGgKmX5^PmaUco^E!GNxA@=|@67nbZH}HLQ)`=bkPgG~+wR9CqKPSkQT)YRdYGSwZ zPqHp0Vykn%Hpook1>7>Ne#4P$U|Ytsm_>U;sE^i|S4`D6%?-CUof__&wU7k$5~k|V{# z!Oh9fM*x!TUP=#-T2>#C#(6Z80oVq&NPQJSO{SMe>l1a1`>&#Cc|TsVdAe53yuj%) zyfMnHmQHnwy};C!*{7_H$qW6V)(q1ba2v{eN^~Ro3}@$STRv4GZ?^NG`~6^3;B!2e z542&qg5Va0ZxHY(>^=NC8YkD{WLH+5s&Gp+bY zJe){T5XQN^SBW@}ow(#K9_0 z5!yzd;&q?bZYOej`XiWbUBELF_b7&ZNQHe=^Ah_VzyNc)HDJ~GI#hBrgqw38)T=l3 z(@c6WQ(pAIM@4@(J7ep?kz>la0s97=@QaKTc;!Utqj}$w<{2{=fGP_+ayBS$Nv?OV zU9o-@MLj&@19lL=0dIk)qLB?g)DWi3b;TE+C$zHi)Yon_7Mga?ms~$woj*EW$gDZ< z_OYeKdB)!5z&-F>XT)vo`9Jdk#LbWNPgq=|bsGNy$510O z3Hd}wMe|SK;#CqoSCl}})cYy#ERn36653c6zR3 z6~Ur+wg54|M$YptTM^$i)|e}2O6+vn6Az=|+*o+@#g(lP(E7ghc`Fb-kW_jeo76*{-Jk(gOqyJBIx^V?Zt+NONQ zx?W}zc*c{QxEZ~oq6(Xsx0^rTP4?`bu)Rr5>H&}am$AtY7fnKeKE$^2*>WZKQ^DXX z-Td^YyNe|HdYyd<3FfP6_#BC#kiX2)m@E^)66Kqgoo?yb`Lrt2Z@KT=i+yx*Srx8?eSA{*(NMX$d0scfzsTA@ePc} z&wPk<+e_CHOHpp3Ccx4^WUqmMwxi3y-|RJ{1{j0KIN& z+Z;iDeN7D^Q<^-@g-S8gP*q(|t=K-YpLAY5|5P|zbAvJv=sC+%qIh~4yk059_Y*$s z!GQoFq%=d0alf_92WjR^03|h#Sy{UPX2HiSES=rpos4Z|)k(sx`?&lROgQKm+32u2 zC1=-Ywi9dv!YCS4K4wRKT z5T!wcNq-ADFfB1dv*LkJ#d-A$f>T@v7tHp(&lVnaql4WSsw=DVHpscvGzs7I!$i@W zkZplU@8b}4qsn({dZbU0QtcLF`>U;JXsly8Xgm7Yd^9Q08yoxUG<+VtrdcIH3fVug zmlAX=UB42A;o9wVs=qyyl(-TK>+O}EZgef9p`|tKSNp6%Nw1iBLQnhZ*f?`G{?vWG zWp$U)l$wze=@)8j-~$Vdl9-P#Qb?ysSH7-BvgsIm$+y=m)O6nP>*$Hm1^}I>!$v>h zcR6|a-hnh;%Bh*z*^*63>Xw5|X6vRX;_RB;2;Q|Bk?jH3376F}Fku2M`*)?=g+_sr z_qXUo#N`o=5pTFASCYXrxn;Q>1-0QPm#nlx^0wYi+J5y>_iN^wnkgoxrZT?1B7tkS z*XMavRh)j&+#^q-=fs#sY?=!Qd8w@^RoTfDb?>_79QDs;mBp}r@mqnie^TefU(&9P zE_(hlEzq`E|ZhlEMxBW zB=f=;GMI6+kye)_viu&-LPFwQU|?X+w*+Q(g-_-46BBemShUh|oauXbWaKv>EK4K6 zBM}=Lx4zRjzA^87yM90^t&ec^@MxBdf#zTJ z06`CfnpOA}$sXZYprcko-`K?$b$&B1%Wo`4P7)?tMN(sr4)wxD42Je^0sxHThP)|I zH$cwW)0Le~8LlcW9<;W;-g|IRju+3TF3n8?J#G*B=Da;|NkX2Lj4?2?V_kpYli{vd zR!?ECgCzFFz_Xg+ITNeATy*46G|w2LG)Q8$r`+5}{|J$VazH0GLS9}=!sxnsvR1lx zPwADYNlS}yV`2th?eTq9ks=iT)p0+{<hxw50Mi^UZ)3x61=aTUwpkt_Bs@2e<`<$Uh8U7G@VBWi)&)@d zDfdzr;DxXz#fESSb8l8A_A>@ssfi6P)i&*6ym^)YEHa$*Cg$daKp61HmyDQLMn=Yw zu`w=5z5M)q8AnIXnj|)TG-xwvr7bIpu{o+}i=;um6g#tC$Z5C+hSb!TdZ4fv^0yS=(gr5NFkf!M<_^|o`(T$ekiE= zB$(!uAAG4E z=E>44PpoYtZVcOf{rYfwa|^3Z*Qa4&z)F$g3~xh2!mN;f5EK2po#zV+WNlqcrKND~ zM_-xs6ES5LzmIt|Gi6qmLuuc?U(45~4pez9EYd#;c-w(t5ZMiIxeB?Mi>NOhpKOk9 z_+&0lvMig^p6fi~FWQ(53=EHo>b_M)#VTK3ZMsvs6=U)%JR_C{IQB+0Q6eG-~9^-$rKku-}Tgjbk2*h-MMJ>J$G7~)~zYC zp-gTiSx2DwEB6*hz&V)5WP61ZK+|%743bur#KtakSFAX-(&SE3v8xeYrm6cr>Mg9H z#7#)%j8)2p*mj8VEMPetm-*%?$C(?dT}#R0;ouGGST@&->DY8i4K&f$&jM^FfX5BtS>OZ z;(^6lpKGXL)nCN3R*~1xAmd^dF^}YsK@y?`kSyR5Ww?znQ^8pRNbc%)YVaF0Um{SR zzf-;4C4JwB)E%Gl{8b*hGZv;&#Zd-RhFC{>NNS08?Ki;l&zCW;$cd^SH@j ze?drEMrLGpermDWD#;eTTQDq@nAcV+*Q2%@s7wRug**Ba3e`$@HszgJ7G(3V56^u7 zaOc1*fPocGT9HBg?QC% ze5fcs+3MF4SV#4cEPS>D>+7>!&fU2!SLX(-Nit@UNcP>W%`Sc0i)~EzlOHkWthx;} z)6>&N(9%7Cc~)a||K>*8;rMdXm*JmYuy^J(G;7&-(HiU?hUUqEkCD~$&Z%F@$mLLw zwu6oLZPn7>edP;ZjF1yS_!PDv#=DDJhduB5UHeDQ%Z67z=`#T9v+in(Kh3C00INUMw)k-_fD(C{MSXaESpgl?ow$0No^s7})$-)x zGH;aUHIKuknY1^4!()*f;nNI=y&?cQ@zCfm5Z#Rg`LWR{^zKpc0H0<4lx(zloaE3z+K&z5AGqXx+Zk>=D>XC|U*jV~e;erICh6pxO}4Befbhj{ay zrA8jxm=0pT<_dA_>Nck#g=;TS{ASHFdP{>#yq_SvsCyRNCKDIdmU?pO*e)aa`x9hx z2h_RNl5_8icwN~o%*|Ow76gYyuai=l^yzpsQ31x<12pkZtBuW>)`V5*%Cc9Ih|;;9 z?=Q>o7tX|WNc7Ig`6aW6IzwQ0|6HR4C^#3#qsc8PPAb%Hs+QGl#gMTmnGID=q zg8yZc!f>XWUFY9x|F-Afh(FnZ{wEF4ZkGKV?w=u5tbbTeK>qx1;?Dv5X?{xmy$JPN zFa0I@6Ao5+kKVKVwddcA959ssuy6l+4Wz`LsQ*;Af3@y^GxU-EenfuT`}ZT#iUOH# z*}PBu%as26;~qNwAEDicA#WXuBprjoXn%sS7PY4&T;d59M5auQdF-h1%Yy&?3I9#M z{qOi#56c0NF8`mEx>Zj4s`&4x%5Qsr)2_c4|6b7y-W^*np7_fCCw=*&X5+uF)CT!9 zx2peUjy=2se$CLo+(^GG+HZS*)2`pdC++Svc30s4xLQ2)G|<+6U#Xuc$KEmh$LaRl zF#LuH^4qigCjM{#SigK#aDT68$;*PX zytj-0aS{C8?e+T$=C>~VHw`dK`zHLSRpj5S|G#PCZ`$>n*#9PTH|@^uKQ4m6`2D56 ze=q)a)!)Sa_*}5ef0}N8zmEOB+W*!|zlr}pznquMy3s75-Ww8tjwD?fvnHDL$$~Hj zq!Rfb|Cg!sf6`<9p}xQO+yDLMkotR_`#<-abVx z_#ZC?a|K#fHHwOVzk2+(_qWyK??vF<(F)G~JWnF>$NLmG*;lOORAC|wlA(^d2j;)i zDjuTv^*P(RcylOHow~bP1wy*;*=eD+8=~bhd0L$c(LVRq;-Eg7k!76hPGYReshZuL zS$>nO@9L?)ftFN}&@YRU0stW+4$iKp_8BdfeKUHhplmfcosxE3K0HLwU2*N%5jzI1&z8|&x+9!R|)N~@@^mF?5UAn2MMa*R%WHMc$pl05-DpJcz zxk+(n`G8Faone@iiOVI?ppnB#_+}0Zq-jyeDsL|upsEZTUS&=+oid~`xAeHNX*3m5e z=9LHH>YM_<@!Vx46ZSFb!tovy4;8a~TIM2;L?d3#xKoI9A@E3xOuYj$Cl1qC7NEio zXrt(?6%T!UZQnoVE<&HBKBzw^{Y}_WXu!VEZe#4?;+d4xf|-DFo|47m$)F&36(-!o z?HyOpcdj+KLFJ?51q0u(o9~aqy5ao3j6t)r_h)s_rPZZ|ksIzPBq~SP_dA+TuL+L0n*BSE zWoaA+J%*MCW{(&Ukm8r}Fg7+emQFolh9J_BBxn3C0L?c8&R1y&BbI$= z=?$R<9x6g|#%=rHWiuBpkRf0f^7+xT$#m3doh_%fVHV<&1VYN{&;aui0~S0@%~aZD z`5vW2%n`*c38oy|ubRsx27K(63^qpauXvBfj>}13G##IL8@zEr^Bg*#Y=k9mSv2lG zIr(DkB#=?~>C;G^wW1>W)z#I7?z!y=&+_`Vx{hqLoi7A>Od!MD@@(a7>BFG%ke;CA zV;_Whb=FZyA19sIJ=~BNOAhsGl7eLV_`@`@5c`U2EHr25HZ@~)uZ1R0?RQU!99ukY ziEhV)SaX#Ez*&w}p@y9kEC)4T4D=-tB{&F4BFQQ;4UMD7aw;pa>U9RU$bgihohuc{ zR%_cVp;+N5Lm82fEqaiNvegs6kX~zd9CaRHpDVb#3AM2?0SxO!cp>IV@@~^i z9OK$q)ZlhXs3yxbEi3n?h(ebsRapHNZ+P(^6HTMRy;0=J`0FW5cwz4)KI+IG4w!Hp zNxrceV7M)sve;0FE?qUCH-$JH+1dJ#ouWR~pw~(BK`v)wWzC@>8zsA9(C0jtoD7Oy z1!`xqE&EpwKh)MH9pVK79z77k+sf*a5>#bn<;2CsFnAIXcd^s);g!ywAI;U*@-^?M zJu8Zgszh^#AkT%f*ub%VlSss>h@~F9-phJM-bYQU_gQxk&M!}Ch++MqDyOY4Q_tEC z4xZfjh}bOe<7AB_3!UTHyhNENLpSac_<+8%_Bvy;8YXhLdPZMuvSKrwi-Us$V?PI5tCJ>RTO$UEEMBEtu^8vK$m0Vw6$H?TVmC1@eH!!_DXS>yq7!WxdgaD{3p{u@fj9MLmqhN~q zD#O+Ul_fRa@xh?m4?N#Zc;c*57!g#;?Zu)g%#v04cl+B4*{`1wyjNytwev=EaTC(P zY2O*d4?J&j7!-mm1RpG?Uk?tzI>Oa8&HAz(*qk1w2B@`S*h7uA_z)xKTJWSz#a$-J z&<4}Rg>0XCpk1y~N}55aVFX`Ud4{RE8exKEFRx*OBoK|4KDKr>%AT9-bgDgqDjK(# zU%cewq2ba&Aq#$)GR<21usk&C*4Agx0KvkcvU5T1NFKWp1-Z8 zwK1qud_w6)P{W6z)gkfI_@_s~?JCIMW+Ei0{bL3oBWOdJ24&IuUR8)tdMjqW2hSYr z507Xk@A)WvCX^l0`qn%2z>D}(la|${s{6}`jUCWj2&s%qDDT8z`h3aAI3mgYM?m&t zP=3kv7~41<7Cy~Xu0YP+oX2d%wEB)2|J#a}3{X(GVwI(nb_)iX9q3CYx3@LW{-P4| zi&GHIKFINnT9+c6LW82duw&UZ_VpT&2ZZcaHg1V#j{`j%@6J9bh#dmQvd&TV^&7{M z=K1)-EZKpYHF$cvvb%?Ti0c6JNw4Fx%-v3|2bh$f&_q%{1puum5>;#~jDsU3Tuh2$ zLf_^~3!W)ciJzz+ih=Tv6Gp$hncN1H;T~@C{raFU#;};pyG>9zBQu}K#XeERfQFCs zDRMKb3H0keoQr;<-`-BIDu^~ihT`T`skRrUJ1abcJ#rQxEl7o?^yl+?!gUI7j#C#> zXbUU19L(NIagcbun?cG1&s`f1d!pC80DptbXU#=)Wt41G!B9@{szS1w(NR>TT>Rry z>{M>U_hR#%rrD{_oM=u|A`3NdD3beo%N*)a)<&jY31ql1cS=shUf9jomz4Cj>i7{y zhlNTFlEQmYauJ~DP+(xVg<>2;p&wP-d1{YqiZChEBTliKE#==W#vQ9L<@p4(eRz6t zs5?8FHNksFET+bU>n+hR&&uO}CMskEyYq>~k0yHT*yscGbEyszsM-s0fHcbf2C8Q2 zk?4v@JuIEW9`j4>UC(0Kq8_jM*+h4}dY6PP^UH-TGI^laf8lcd6ac8Mdi|Ywu&lm1 zLHFT(E-JSTYz3|FbA}WdV)GeVK9!DVsmQDfd0QjzP*??UIy$vFI`iO2z68zWXj?uF zr9d{~#aG|ncJSsSiL4IjJ@HyPYS!OxhNMH%kDCKsGs(-4#FYGK4LTOx(UFbwImmM% zewc_uk_JIdX3(7bItw;5*467s(n&!UJ)CfXprjTUG344i5e$wQPBk06OO_y-w_ech ztzuN?NF|wBSy6&s4vIcb&`eo^?m5x#eYRkoR;CGThSl{&jKhQnUIiK*i*hMaP|OBa zJ>sT27Z4TVr}Oo>RAOL97krY$OJXM}Dg*vnIZ*b}K1-0MjluW{*-H)IE0==_xJ?~I zefKPcCD=z1Re3!(xwwwV>{9t+5UPFdwQ4$aRl*VkVsx#xdcu^E!c+KGp)QWW|?fy-cb z=T^|Lp?5v0wGjr;S><7g(FFoPs}DlHQqh>z+i2()YlGN({hr@FtA-@;$vkK}j#m{$ zFekRi791(U9WPNcTn#H;0M*0l8-Z{J05-0>OJ`jxCO;OC_n||XCOok-rKz{P z2458O=yL_%SwHS%mg})$ye%s!@&E8uR~uvG6`ngZFZB*F2<}FwIIQ|0a4o?UO%`3D zRVq#7?#tW&0CG$a)nSW5Pfs6%h%xNgj75M*CAk2$Wb<=}GKbRA^R}CJzm@?p^3rLC zbai+8GYM8@z;fP6eT4M>=lE)rc9aJ;G;JiFXSf1)S%EB0FW@cssY@<;=0m3a*N+S90K2mRv;7OOYG0x_?+Kdd%XgBQAMmLFdB(cxoAvY z6~caWxuq)B17PS=T^+%!+`NINghrvPASNb8*7D=M!&^c+VK4~R@%&o-(GP?HI^uH$ z@?EADMPo~c2Y^J^CTIC!xK@S}XeJN5auN~}xJVcdBKnITclGuB2L;S{&mCxp7N@6E zAh2WZkvH-!f@Fb$h!KQ0SYMgSGOxYAeT3*8F^;9s(;TkFTg9B6oe3y@866!Z5(_BG zU}8CjqvyWA@Ji{DB^=hLU#f+MJP3RkSw6=pYyeiD;kD7of5Bow4B*>*`6r~6&NS*NGY+oZQg z$xW{NWQwoki4mdF*t#Bg4nQdw9F!(M1%Q>Kws?8j@Syyy{`vmmz90x6AHOZa&F<(1 z*7Ms|-3N53;~(FtCtDc}v+YTHVx!RImQ&!Bkg9E@6{Mo!&7HuX+G+iu*(D_|cf<3xeLXy8j9c+BJ`NqcheHrTUm`gF`W1!%8(w|tm8Fpp6~)@vSo~R= z=&r>20YV^7jg6i^-8qvjHiew;hFBq&b_DFf(PMYA4&{TuT@zss5Q#gU`>l}gVa`bd zER{cp6%v=Y8!L3|*V>W_pJy9bVxMYsN=vSZIFcw#c6*UgteDTj(&OHe`#{~eY|D0c zcZc4;XpuT9dNWvccsNMC-}V-9hkGHJ(*~=Rkz`>8_WcecH|ydioR=+N zF!~yjZf(+MSx3cGS185VrQQDietym35n~(^lc@d3RbK@a`AJh1%35^dN}dggA7AXJ zt(t6M%UM}i#HE@ofA|@riyI3KKx=<4xJCHfU>k{ZR&lQ+cpqTTcHAhX*G!Ck*7=4%+pmts^Z8x_cR z+vO1T^YN|4^Mjes)*}*Ml>6!mzx!Z|B{D(svi)9{9KIaScPwY7$oz=BU7F>p=VNyY zX{yPZXYrg+NZCL$FGwC{XwF{epp{01mz-?Ej88WbXk)ygodBS z0!Rdl1EEJ>|6sQJ`c7*XG0?)V2jPb`4ETWQBXVkDd0w#jeWhzKGcwF8D2c4kke!MV z^-a6MF9bWf^*glf9|#8h4e^j2J3CLQou?bUzd@zERfLTKC$mzrAmKKc>*yElbF?^~SDm@mu4Y`Ol7_ zV17UKS>CeiOb|HY`|MDmQJv%CKZCkU#tvT0`XX6F+pnjhWW^N=Mprq8woe-zr%&Bh z+WSOWgfFYbFew*XY=alGe8Gtm?tX}iHr>qkQEM^!j*+l`2B<*M;MLw*gk#={7d>k~ zAhldAp1vU(ytaXYE#yFC<nApesw>0Cm)YFF!I!nA zz<(f)_z?fX=;D`tf5`!O+3Yq9@qm+4Pb_e^u8-V>Q)C4%}{_#ivD>2faKLBz0)wjQG13&!T zfqcCHR@y@tdC|aIjN2$Zi67ysQz?V2 zLgl8^#&Jl*?s$&_lb3IQn#cg29Th zG}5th4^C%btVysald^5Kg5RBf8o#(K-S56;InOP;p7$NQ-kk*sy;=a+sNGpbbA8}o zK;3|OwBd99*AKO9KPQ!+3iaA7gUj5LzL|)u{X3tUZu_qdi?@=^f1Lktq1*T~09Xw0 zbRV39_%m?j!zvA9y46rWLe5*r(~_!O%#P!o!Piml;GZr2YMOC3>;ZWY2?z(b&F%X< z+{z#K^y^Y@h%hz?j^PvZU`vMXN ze$I{l+dX>VuwSP|y9GWj7H8NGTr)>0Fh7Z%a?`g|`H620dN1_6gj|LXbo(Yu&O z-W;e&>g~hCC+(VG{42=Yp0Ib#ZI>IzmTshUmvRqJCN2e>0W|-!7p$YA+tX$b4o)gB z@z!XDd`VxCW$CvM%r}EBU%yhLnzG+hTy|}8){39s-)~vY(*N_+r8TOx4ef|H#k0W@ z+AW6A74m$dsy^c$15C{C+GF_dX#zmf-|~OZ{;~7rADP^=Q{cosw3+$=9aOrw zqXojiIxmD4b5QK^8aQ0tJr=&R?&++*f_=K6I$3obb#;?O;<>n0rH6rq0XBV~N@~1Y z6aMjx)46%j_1NqU3VbVSkXZIh1{`D%AqEG9$rtnQchB3m;jWd6;-Qg{DDUA|RN$gD z8eLW=7Dc6eeAd>IiYl8C5v(RkWnK53vH9eO1`kVCe&`A&88zJ@OpOlJT|}+$64^7`-lUa`|x(Gv>b6G;?`n z=#57%I6_jPz+|Q)g4MnfflG1XSQ8hGM2@sTp}nkDa@SBF?~Td4 zvGt+3?g-nNM*18z|B-WVLvXVXy2kDSp)XO|u%K!L+hS8{8KM&YmcC=_)f;|D`dW15 z%m+o{V?p=%kt!IccmLAOIpc_8N@Ui1l-jPf-xUT8 zPPD)@>36KrAvmCF{IddMdWDhTweYh@rl zPr#@VebDhtkb}XOo8#N@+700@u~n8Y-@8__uMXc9R$mKs9@Y6CXcBpF=Ga@zkI+-* z_K|k!*6x=Jf0hs;=qB0qx^=_&Y^KxVz`Z#?bM^)HF{cPr)Uk-~X4UBNwDMM!z!%z` z=O}!A8_DXk-JbOq&Ey-sxo5&?T~U+o_KeCVu)X;}pO#`EBBTUzL;CS2N#yH8mA8BX zdZ?jR;pzCk4fH-wkbbG^gPw}^n1iRE)+RE1WIzGZkBW*p6yF!gudW$}irF|J_oC(G z5x$szp_Zx}7-Yf|LCS!x$us3WruZI2C6JsDI5sqO_&BxGXn78Q2OZCG%CoF_Vqv?{ zePe#HgIVwJN}ZDC_Elzrx=PUZ;#G$iH?vXPna`1ik1&*=-zyg|qOc>21sO|VzE1Gr z(L=}GVK#o8Y(NHYgl297dh-~i`h)e+gbIP6ko9-~6m!OC8#g!XnA0A3e>~km-Ub#_ zWCb}IcLy>G(TKh$nZ3^m{DRTjZtw_8Xs#3Ihu75+E-g)k;mK(7fxl9vb)TBBXPyyb zt3+A}v?|3Bshav?YZlXTH`BSbe336AWp&xxJUoTUZoC&)brrG5;@hhOv#y4UvT5^d z{?U{4wo^AvVi?Tu#6l=vOpm_`ei-wTVSpVkIFDkxZjS0B_KqtuT>>}nKVDLIj8-xx z;^W3iMD7cpyf!y$FDDXCz3%mMZY0H;7JFd9=#0z_Em*2%1Q|0u9y z<|*g$NCJD8`-LfGZVfC3)DuZYR|X;CcW+>$&N7MsR2rRBr905P})Ub&S1> zLUV6N2ARLU@SgVEffN16hwz$C(ieBfnz;-kVQ!@glFd0fzz1ySq|DiRrIoV%dZb^CSa5U$&Fx3PAGIUH{)L6a+yv*|bIM~OCvq{Sg;Y0Sdz zl6J@mGhhk@cCa}^h0~JXRP2Z2A9!95>Q00aMY}`r@c0wu^yA$~;tJ%uYnF4?ypCWc84;LsA0AhZtz(n15fqA7TxF-_Bte@@=qO$zb2$^Zj+ZAXplKZ z_uU)rD`=fE1w4uL#l^&5c~AbFi|9o(!N-6X^L$j4GziZ~KxlB7_B~5tC@W%O2+s6O zD>4HTI5h0KBcQG=6KviauT*x`w_3JkBIcsCRmZ^JFzdw;q<&GYtJZ`8Zo*wE%7 z4x2L3HX51gP}99u8+Ww58LQrTf^e@-gEqC7X*c24-^|aUw+IKeQg*-i-hGSN=DoUX zWiKEimoEoUoqvmGgr!6jLkmndj^y8{Q=z2!;55Cio_>E5#&&GWCJSOj#eQ6VU1RmB z+Nul;O##KNHoR4Ar{E#+it#o6Qg#hoq)xF}vxsQ#RKu>(Oz)M6u zxZKGV(7au(?@HmCEJjOO(bCjc7rbV7Z+3KzS7eQmx!|5gNE7>I*gLONfskN`CuSn4P1$i&mhpznz;cq%7UVL#wJnZ`DxX0fQQ=NwUU8BFfa z?V}Un+&!p!)1rjPmhiD#-$u5G&KG+MkZuo!fvH z0lI>nWZ6mNsdk>z&Pd1(G*$+zbx2r7qsy_Rx_^Tm>&49Nx>lec)$qi4&Q^--r;gW_ zoKHK_FR=@j3`X211EObkWZ@2Q%S%|^?EW~!)^va}X_NvX4WGH^T=WTsQd&;J*lYyv zdbiUF{BgluC=y|=_E|MiBtnT!>+So^il=GS%#W7VpK^A`x1frh;h14K zu+ZjSU2%}tPq4l>&*+8^93uxI-FeiI1%`WX>`sM5RStgkxYK_k0ojNF_f1wj(ohF~ zIKO%MYrZPyqXd$G_%R%S2}=NS4-xIL78aZS;Xw+Lrln`mm@h2%qY`c92Z{TO!`I&W zBQnM{(bFbx4c_KzG$2%dTC`q){sC<_BER3XZTnKf`vZ;&T>2QIeIkaEt_w}VtmEVi ztlEr+14X!#HDrnIiLo5Cya55h*L!rKqp44Kh?0><{ytDjP%j0Tv|&_RWrq>Da&TCp zW9`*Gv5UY}jwzG!aDNVt zfAqs@I>_El0!zt;C5DiCBWWcj^EBUNPD+5s95LGe4Yce8QhcnYXdCCf(pP`-ts+<= zxS7X1v75x-HOMi%;Ffcv8`w~j1JL?GQ*hMy7?pNP&@C_VJr)}&Bo=?PH@M-v+uEj= z=?QQ#$cfk3Ec$>1g}m?*+Y+`8n!JstXoQsN(BEt)!J%nxoDgF*Zq7BTY{6~Y&ni^m zC2BwP=ZP8GWTlu;T+y(3Q49>#Pm*_=X;6oRB7FMHq-k0SWf@h_$*|NPdtgCr>W~Bg z(uR(r7ccKwH5x^mVoUxLRY~0;w%kBCrDMqdpOc|)h^hD;cuUcj%N#H%p= zXf-K1Un$sQ^H%=i-c7(T)k<($#Kkz4yRj7~q+6Z z{Xs!BRu)iieV+6iP-kjf+-d)FZT4NUSxYUb5& z%UQqj5_X}^mSU$@$g6$7nfQd6zL|6{_%nCc^UQ))yI~FQPbzQ1ZEmlR76?6Xxgi!*HNj6Yb&O zNH(Te6Bq6IOj-L1r0Lg*ehO9lD5((r)(x?|73vLaQ$@=Mh_nUEFoGpiz=sUyD~605 zx#En}W>}e1U597%!)Q&=*`oP?@-daT%zC^tfNO+`8bcg~LidJ!n(3*=m;puQ2}6_1 zwdgiV=c3H5H2I3U3v8W7vIa|RGsT@u{wVWhI1Kd_+Q}$AluF~<*$#NOoVCh&IhQA_ zt??~T%$6K4ql9i?5XMQmhA8sCjKMJXUBdRX$_S4m9HcvKD?KGrzWU%{wfR+)>Ahr1 z58f6ZQ@x+|Y945u?!BBmh^P{g0zsje?EsBLNzvDchN<-kT7kA(aGVckrotrYBs4D9_B^OWh!2S*PJPo+_upJR;mN*(_ zoCfDJ0JOoQPpeNb;P!pr&TBVQv!Fg#M&WbmNnH!V;yRQ}lo(-%{w8&I%=_AG0p5pW zh0iCef_1F#QIL_<2Nw!k16!|kD6uZ}(Y+H@QTLA&ocl1Fg`WGRlHV! zQ;$@}YkR)a$JsB7dq8nt6U6f-g^$wa+G1u(IMY~yX=1@&=2cq{QE@;TLwEU84DN$M zZ|<6x#G+NYB(?EF?51f`wl&2=tRjYYdT)o;I9D_v0){kwQ66Y%nhJgI=@h$-w_X#T z>NZ*D-32vcIu7zw?qkUl^pHTcc!zqo3hxq+*_VCAa=vn%jpqQ=_$9eymP4k!Z$H`4 z=@r?HF<7WUI8=CvEMMi=z}o8i3+=gk#v=4dS&i{{`xW&SBE+yPJ5wm@COaoyNrsU! z7efgIaLxrr<5QaC6k-v+Xmz3qsCj`(%k<;(jVN{tUmg_ou)!cN9IX=tsK?!(;sbQH z`VjgOcH1OE4DOCuN~29R(s0P+$3$q1(HaOrYs}I+@8{-XP^t`EP(`ohNUMpj&rkWF z9s}X1fp0F#z1nSEs2H>8#WutU;&bgWP*r}`qRPvQQ(YdQYD@Ypn%2*6vLKH|auh3l zQ?hOw3>m#chc+TMyxSd1&4$`uI8wzXM1LM?tNl?67ud9TJbpgVxS_L4lC?ay!oMpB z^I->eW(GhDapIDwW~njR>CP7-pO!ogIi>HeiF-}bcKY61)3=ob;us*62Xp`upb26? zCJjws6Ns|v1y>(2?8hUecQ1H_mGm~EY3HWX*oSgH&pP82or4n`kcbMIhinh8s?KXO zOY9)~3!$2WLTpFbdb^7@?JZ!+W!%H*$*yhpjFWB)?{JI(tkuWZgAN-fzKKU>A;Of4 z30I(wJzMnck9$yWcHUw)@03~ZlVZJOdbkr1V<3)LsvI39O-IisP>e*drPK17Z<6^v zsRz8+LXp|Fr^zbrk|X_^2d7BXe+SvvJvd$LaH{`{(AH#wBp&*%AbKwB`x97Gy9 z%clf2=pF~&ScBXV$VEuSl)#u`ru_EjIvqz#lB?7}lRq2(M>m{sV;q^6N$SncT-PB{ z`s&lrbn|)cAyMuJMSF^L@9DqyW}{AI`7dW3GA?4@-dtYRUrTzHdRMie3d|YoYiCDc zNHd;Bc{gf@P(`ltc~$tocEWUyuEnr4X?cnGiZF=5&`6>H%h2V+_NRo)P`1W~x*G5X z_!~tSYfjOcu)Z6CD}pm2{Bk?!SJcPC^rwsZp%m$7=qooRA-^AxJghS zG?Kjv*>FW?F-?>i*O+Bo3>+R?o2g$ZHedz{?S2I)OkEt$3fi8H9j&e&NF>RWSzb~9qUZq&l2{>MI^Z24r!Gj4z^8OgB#yt?fdZ_s}X+&nwd)}a1f^@B@+C^T+ z{d{7Zj^UFdeHS1y@Qnev-8b781Jh9H*$YXr{>Nf+45N>?Mp|>ij)vKONX8eft`))w zSUQTDkuXIc)^U7fq?nli__VVuL``_qzAqm9p#GxU zax{6icZgVbPpRylOA?_m& zFBhXwrquJ2hU8>dAdP#T8{YfBrO4IR(L(TDi<}(8+GB5*2l_v{qals$;60CR`eN)_ zaw}hrsv0e=jkkR2kP}a^C_}$Jbk>~l-8_@V(3;v4Tu^}V$pE)v`seOd1F0JyW&kiF z5B#0)VpvkD5ITRVPDmyEG0DN6QczUFC32u-i0Z0n(7k*>{D2kY~6O?ulUE3@fpVuDk@+@-Fb6Y^x`2A1H zN&uMP^y9~~_e&yxm$e*E9AYoHaHGC`%kS3ld=5(VcaNR=t!gy{!nmyoFnHH&*FZe?STMsgO9xs{ZIhD7;Xp8-ILkJZhyW}*o-w|G+&WjPJZ)<^pd{CX{9ir#s677Ypa0i z$P>aJXUB2p9FxI3To(U~2VPtq&xi&u=T%(DH5^Yhs?%#&HA{m!c|ZT(wzwSp)!wE; z-G|9^C%8}_$KvQ1pH;1*qdW48f+004ynVtvDpproB(3CROdaH4Wgpk5F~ssT1u<41 zJxQ-h=iEQ?>}kEzVa&b~sR(4Ge^u(hW{-JX?kvXx9fbG2#r`w3LuuyA1uYQl*}nhZ z@s|;uH_4dZikI?wgRNg#eK)T-*lw4EcPtRMLe(&;^r5Xe(ORn1K^witEO@GqPUF;6 zk=4I6fk{(Fae*-h#UjKqEqmIBV?QZq^S^T0PBNS$U*Kd4*EL3Qa6EGy%Smp&V8uw4 zzB@Qf#w$7*;g>Qq`1E>ftesTKRG%I*;6~4smr=wjS=B1%*qsQ3EBv+qHc|fCMxtNa z>rs`cRL&njxzPA`d&4tr-C;zFAK$T+Tnp2p-z4W?xr5^`3D=+T$TXbmFA^L*A%(N+ zb~?EA&Azy>2zNJYWB3n?7o2~;;|>fs<1X#g-eqy)=DopW(c!!`~5_e`|8wl$bc= zN0rtRk*E2@J-Q;{ox#$4?r5y4rig#}TBnqpf#Y~y`uy*l1?9UF;$wV|%rHOK$RWNj zFL@LNevaF(IGrv_4^Om}BWPMwU0R^4y{%6sxaG_Cfi=(LUr54>W>m#rO~ z=<2O#9X-S@5S;%-jSE$ISJswggisnUHu6eAek6I9^u!3Fyb|Oqe{9GS?voEYad*B& z>@Iw!rNd_Lk~U)*8CMb=O2_`J^!Mi5cX7}YyYo~QNp6xxwKrxPS8Yt6uXuT+!U9fsd|g6vzhV++VM{zXpX6U_3Cw)MetCk6-K7+t9=Hx( z{e{7_f@yz(y(wwu>b)p4zkN{WFxI8HH^A_QlRfa<+5OsX-!%B-Ct>!T z2Rt|5JW#hc&>r@*H*3x#*G^d7`g|c&0-kSZaZF@(R<`XFyzMgv%lG#g577(}lE=0? z%#0mkfKp(0d9~-JDeGxZ z%-++1MAGGw6Y1$+DK@WmUIl)AvDY;o_fswwH>a*2Y*S0Xk8Rzb6^Aiw3Jq9^4hGC2 zeKGAYt&H=@qcjy>k$q{r3a7=6{|5dxu%?ZzQ50ruKV^t7`I-rwhWm#CJQXR+TNnRXP|<@vK%AT>DT6>YDT z0ZTUtAO17DEx-YNY@+Liye@%9U(&?=W`x|IB*ZcqtS9@Qe$ z1-hRy1#TDeH8i?avoQOZ{KQ^)At74+%w}rJ0KM(F2}1`%24sU7gaHwMhslYHf!+JM za=+XRCXP1(RphXw1`$W8@CeNEh?I(6DcrT|D)uw$?cp%3HQ-69aWZB4+KMn$sWywr zr;N1c@DLQ^xLT88s2K~=N5Q19Y9*IhYhp&mLyVY+6nn%=Mqu(-^S}d&S3`3-f6~D( zX-)^bN45pAY&KVNV%3ML=Owws<1bbVITXdmc%w;2zF-|dh zMlcmNWvJ1w_KB~qOMl!td5o?4xv+8rYF?818qohD5vl^S3qgVc4F3d&&gKlXa$JcD z;(nDWN?6iUXf;W>n8puzX^J^=t3Ff5KaBne!$r}Dr-z4!j2U1)OSY;nF7n%4@RQt8 z0v8vSnbMzO(r$ke{+0{ov0nQUFl-sVS*(T8g+50@GO^eb|gbMR%`oMekXQt+u&xBFzlD?Yv{HdsyC_rBQzG4 zy{wE`e6HrjzAg1+j~JLo0DZ)$r^vRC7*XzrUCo)!Z%l;KT4=>CFJcyAd0&_bGqd%i zUXas{ZR;S9;%YA*r5K6e=k+z=*rVE0>RyF#8aIr+t^a4lI$|&h`uO?d$B!{4xNL## zCsqR+ofosxALFXqO7a3%wgNtuHy>9`NC9 zHx91{Mx3B^jK^t}?HDYUWX7+Y=ddQQ0hm!(x2Z{}5X3cSPlB!y#uLTHlHrA!)9h7V z#di(Fn9mgjf(F#sD=uc6gRl|hV{ja#es?w|BuJ%{jd9`nVCWNu!88;Z{40P+G1gh< z+ESVNH&;ntiVgNgNX(D%OE^rYPK~{>Il<{X+N~mvZguW!JJxvjzE+|p;YkmRyk%#F zl;(4uZ62(1A3Vl3px#=qTb=g&PPmxlp@jHn9(kahCCh8B#$G0S^^rju7%_s3^${*~ z*`_yJisK5VZY{GopuVhF$|JPmJCaCZButZmH<6zE8~80wPg@#owqt=58@N72*J;d2w5C2nnXa|J zEO^53E-^wwpcutSOPC1xcQ^jk@f~TB>*yyx-k6cq27wPASg!mH?g*9tew$4iZAA%C zJ7uR&oU_X9oAW6Z6V*{;s-XhXuKxP^bmusrj&aZj_6S2utLiXcM?;th(|IR{1labs z@ecDigK`|>17;sG8g`>BOzXi33z_a6o1NH z&;wT08{=!8z5V|R43xmS%E?bM*;83vWHVBtC06B5THE+bvzOmnUr4;tA#03x*!}JQ zLgbtG-EHy-}H=99YwF$_*74MKX zEz@;lE6UVbOts-PA)!*~h`_vjOX@Q34_mi2%0tDGe(vfx(?VbEpa2${yQ<$)=DaA>?F3h5;u+GzXA%nd2aQn%+5|bMc=kZ(2 zxZ;oAJG~Fl>Or*sFa68_QrBYyl^eds!M@iadkiVh<29Sew7p|By!FZhJ7?%pCx(g_ zO{Pd>eo>vpk0A(*L3)a+Aj_@wi~`dT?vNKR)P+G*H{6&-3=6gsV$8@-VEc0ebgI3} z49}hkJAxBq$X70{gTZyxtVyJen3eC&G2DVcUbrK|;o^4Txvat{+nG93l+~)l?G)?E zkpHO7;~=~<*yD@>N7I2vzUCzO+h5@Avf@{{)q!_!FJOz-jN!RdE1MyWv*G`D0K$PY z$+(*ax@kywbrv`JH=^T8%JVbF$(h%aA0NE<(P6C3U=rQvsOeu_wL4eU*zZ-jG0(Lp z_v%E2gvH4=B<1t!*2ovSkBfq^>vRf8vse zTfA%*{FdytT9lo6A-puRtVpe1HM1;so1;8X|4*Au;-Qb0oQN z5b?jgy7x0{l7*W4u(oUONB__Fn)j$G7&Q?9-&Helv#qy)W=8U(5&xfUK5p$E`^@A? zRvGo=Q)(lf2WKao@1HH+$_3l*q(r?(ciNCtK(kG^Z?$b2x@G0PQG@@82l#5iVRRAq z1W5meR6#z7N0g}`M`IBKzX;I6=pgK9sPKERH6YlV8=B`cpn}|l6#J|)lcQAvL-%Bj z1^@rPfe8q8T{3@7xDG3rtW6oh-t*94#JU!+c#n#ze<(1VTghr_ZqJpd+GzVs<|3mu z^#@HTuZ}{AxQT-w{bwep%H-Vu`E(Z}b>p5YfK=`HWjeU1fr;kUN>rjF%D$qnT zXY%zB6xY2DD-akEOZVnrcu)dOmPP^kd~NO;^Oa8Kt8-nddJg|E^h4qzg=gno#^B|f zftsl(Af(~@q)@!k(n$gS%szi$2vmd>fjg z&TQ2tSE}n#$+t^um$vbfLa#;}om{kLofJBjMpu)!eJ%W2o&4ixWqeCb+-J946;7tr z-Izamx4iKpm_W5;VL_iab_VPgavwFkXNGo^DI@uI4CKKm^hU5?V3L3Q!U`o6ok16( z(@6y;<@c^qk`|Cm@qL?3i8-kD!Nr03-mm2$D*ruuzumr>(l-ED?0_qKL) z)Pna|35kgG7^Vp?A2o}GYXn86(PERa$WeLJen2A|M65AKZ+^PCP4pSz{MGr9K4D-z zv05ON=ut8s=)zlRvEpbyV!YX9Qsak!!dPdzW6-Ugon2pd_gf=#^R+%+^K%dyTbCOE z;F>DbxIpYw)!1(t*uDQUyD?mAUks{Q&kJ_mbmcP+E)9<-jjVpF zn+cc%i_N_&bm=Snglc|9RpJ@6e|ssDZ=fbP&E?)-=l|}iq%SGy(P7XRim9H|kYgN} z_V93Qs~2DpLvvBCI~B2)w4cUBK|m@P07RowQ^CiH(^(nOR79OL`-d+O3WbqwuQ_12 z!ST6+Ff9vo5q&g1+1+>LViUORr;BeqtU!0#21rT%X$0 zLT=A!gadqIrLo`@EKt`nFCRtZ^Q{I#U|r-EHq`nd9f%!B0U&_mKq2?|$LO;PZ0M=J z+#^7_l;J0Q_zxu)eCTB)V)UstF2Famg&5vz`S6>H#_l^N0KFt!6c61K3rv0w-5r8J zzos6Z2}jEgcf^DGsUuyoRgh{Q2Qk4iA09EnnKLkM3;9XnAlLy(Eq2B z+oXu>dCCZ^lLs`<;(^JGKq$?R$l7Pf5Zl)bUF zNn;UwMdFgaGSFM25oJ{Tw5y0_n2%;dTu&kFMN8@i^2$AP!1x_vVb*r_i1EnD0R9x0 zmK%l}OeIe>3=tG<&pLA@$}v(X$NrSX>$c$BmD zyo?0ki^Kvm2B;v*8rh-l0_aHFj|H}Pe{Vp;4~FQkyK%rv;viJ3q6RS@r3xHnBi1&< z{_F0yvXG=KWI@j}>R^|3HQDI=^uggJ!GXfsP1}Ao76bUm>aFd*v&CoL z70KI=<`GIj2zO(1>hGSl=K}buZxQofRYt`)3N@~X-frk_c(z0`Z61YJ6XzOCHWx^0 zdt5|CH;R>r4Y0HuQ=W!FX zew+LfgZ)kTJ1PveH&wb;HaX{2?M2FKBe}!!<)mP01}r9_=ZBn_0Aqc9FdC{ph9PsE zkq$RqzkvR#w%uztD@XD*^iiHHkDQ#`f;{;Lb=Jzt%1d8o{*!cfU{dYHF;>VAG~2mp zo6lq?Cl&72rCM5x8}FS6U~sN~%mq00UkXM^BZY}~dHmopA>>er;F{{{@|RwML9mRb z<;nTs%GCV46gBXuND|P@i39Cmzz2W+V+IBuNu3*E0WOG5U6_DJgAPPseF}h-3Q2+)nK9tGAdrvHd zz0y*LdJ@U2D9gQe=D@bLwjml2h!khJNdVjSQm1fs32&8DQGV}Ry}2$Zq$T6(T*7o(4I^N$?wzVzUSwbT}n|}Gq#1-}a>FeCD zHhZB}@k=dlwBsH`_|>t2rR;2MiqPZ`1EAvnl7L}|L2&WQDj$p_KVh=i>gn0jnc3Nu z+{2&sdU)tJDEz3Ef-)?qYpAj>kV5|1Zzbd^7$`g(hB&Am0Rp7>2;ezFE}&vwLb%Ew z;L*IIh%yM~#{@S;DIrT<1Kq>X?uh?lTM0Re2d*@#r6@SKgS+0>wpB+Cef^E-&WWh* z^mPW{Z>+Q@6q}_7oBx4;mjd?#H(9?3w%=eMbwy@tc9C{0K3EbD;@X~dJ6O8XU=}jx z8hVRM^ka^P)KlIM^{XinGREotYBid$qGNhdzas1vFl2i z`=NZ8^Npx|2;JXeM>jVvtSS5`04Q8(>a}GOnvFSO(0vz8LajnZFl-ck`V;)W&*RzYa9<_7n$B-E^xpqLy|hdk3btIvSgs=@m-nAJ!V_ z+_15b?#C&VJ~|~1@^${h5UkG5esjb;;IMQqCtKQ)FI)#fFEI<;lqes4sYRp1)l3*7 zW*O}Y0C*08f;I*?AARNJD^6oT3+;hVf^#^9;3I!QD85j0#@5Vge81^11S1wTJ-pt! zPfH7`!44t%O!N7pnrGpaP*z@t#RVn7Z)W>7PoZtsV*i> z!yhFGKa>%}`FZC1(#F54H{F!~&3pRn84m^b!X(;HpSa`CCbadR z0?8dLNtL&JyqTEE%sB-$c^!Gey$?^V(Qp=F%E3r>_o)c2 zO3>nN zxHim)LPfNO`%hQ~RO9t81^7%JA{O&EsWqbAqW=3 z{m-?k#aP7cA-l9t_qZC_;9FTs{Sf-EF*WtQ5;@o$zVSx3ZQoee5^KANlBKrx_DTa4 z*=(wA51Fm@Wj_m%=pk$VYkqKqJ$O@$D0A(_nPg$IkZQ|Q@%$5g^W+V?lU!~7FmpAJ z;~e?-`x9;;Nu`Su>(`6wCLEIz(?wVrZ4xR#cV^ikC8TZtCmQssu6+MyjK?hjpi=RnFTKjOw(!NqQ^ zmWODw`O^-dI_GZZos6b)$8WpStZtamlwdf{EUEE37i(DCre9d3j zOY(j7dtUU)+3l?l9?NF+o=}>Znc3w}Y864n$DKrn0RH;3a>G|sb%|_`eASO5)!Ala z89Q_ReGG)%ehg%&6CcbiE^22-y;vK+iwHqj{n!2iz>~`SdlY}VI|Yl60bOcgQZ6rc zgOV162VzzI8tJVWdA8JA58db-HXaNrU?kTh!E|#Z^5$Ky_ zq;gkLtajzYM-6jINT~zp0u=wZLQ>ZPx$b2dblnXboo|2h?%&|S%gY}D0j!D#@i{e-W1VK{t9{8 zp|nJT3;(euV#ApK!n)0|APm8Q59TrqK@<_9pQeR@N0aEEE=N%D+%Gv&Fe|z;E&mnJ z9ldJe3+$rJ#Pie6buVqj7dc;d1p}vSk>)NycPB+8w|*h9Az*&8nXA&cGk)h=GZ*St zRUlkx#WxC{-BNfwqdjP)l)u-9H7|jxjUomJJjhT+#?ZESfHb>glBoOo`_*oN9x>wc zl)~8Y>BsV>cPna_dsBU$GT3Q`LAR+vsP&Y_#tVmwqtwj>x6ZN-6Ypsgw>?zWsS^o! z{*HWK9aKy)jln4d;(4bZq7juiHZ~RtVtkqXbh2X_;kN}iMC;7YU=lf#D0;cLKHj0Q_Y)X2fIIanx(vZvyiOyQ4_EcC+iSC$C z|Fidr-WmRs-648w>8Cj0TwFLY(ZBYI7O;61IurDRnVES*XpamJT4y17Ii^mkuaKzx zLp=tY+!#4aqhT)siFOWUd@!(F-(5ZVRko=mr6$pPR0zZDo$Cq18*UKlIY){9OJ^mG zFVC`HR~YvGpRr5CPqb!w`BYp@;G5$N)%}(CxS<~Wzax$*YS)KM)(w^sG zDi?=GsXr*|B%C5!LJ?P|?dfR`%<{6bO^$Zv46Co93XY^;Oa#Wj>1i(0D#>6X@%)-q zWC)nu9|RaY#{h@Iefa90Xl=Zs$jW%EM2tB>6;I-c6Ofsn+eSTlOx{&A`q|eNp??r_ zYX#tfHkD6@a4lWDa(T}kh%(z_t4R@JoO_4({4njsJ^_!%AE}U6{b?5%rKC^sR}$z_ ze~ap=hJ5OFLq2+RPO{?oX&LJ)jr6z=Ij`Wj`lXVbtlc$Tu*s%f>_OhMsK&nvvj4}O z>#}0%NE#ONUI{L&tzNV^5h?%)7ypoM_d_|-fsdu0p=IG~?wMFTuh^ab-wJc>wS zy(xj+Ph?aJ-E(9~E$gJP!@)%34l zd8a$`$)qf@QDp_<9NvL^XO;a4XJ$V2Yr}E*WQns^&n4~zU+s318wrFMj0%nGBGs(3 z{`VuAIjyF$@yVcN1Gv;rvZWIKg-cJZyG(byJn*WY%dNL9KHaNngOkGt=Xq9MN)Rg+ zpqZW&H{r=kXUSAh@oAW|Sd~}<^yLm-I=w%8TSW$as{n}oIWD|68c1;rJgP-Mh4rGV zDs;6O^{FBXVra6gd6L4ZIJUY$_F}3Jp`t3q+;2%d4axtKrC04*aDUs=_7})uAuAIx z9e9b191o4;=WHgRoH(T4zmg_sUUmo1nhC(v=U%Uh%B2(e^5+otbT%V2G$O2q)(jPU zbL4_9UUb5#lr{$C28u2E{<@D#kZ0C`<;r_Ym-fpZQ0n{n^|OHo64KLcDJlsQ0y||_ z(h&)PF4Ss)9T1KCBu_@bo+}z5ZM=19$*lx+usF(HwJXckmM5+wy!GbOAJ;h(O|c7hyVg{_(PS27yN+C{X@{!+^W*4DbHy9#A{!~wg8!XpC+=2gW^aR zfv6^AN5$Z4Angoz!ExMRMsp}HHAivkv@;<0@gtjNL>2gIL=%Z7v+Q|D$qV*~eFiP1 zC&t^P^`c19&|pW|DBTmLV1U3LvrdgKlMoUT;rX-i+B1*5tNa_DqAq^hVfcN3qVc1ba>F_G z4Y($)oCLnlo@jY+pFAKtGKadRr%drjzTClGA6pB9=O{y;CnH9&2%3wz0vn3KGHXy4 z0QQZ$-jB2$JqTD3O%d4x0)Nuh$WP`G{%;t_KOQ9l<8*j^fJVW_1t(89?{;R?%(!V8 zyv@@WQMOek|Djqs$``>gI`?U-CykIDMoD1I3&u@FWJc_~bVhzp{^`Cd>XJ^>yy^~a zy{xXdx-4PodoX|RDV!?M_?I#sK{ttQ3AaD4S@W{su`^dezNNa*_^SX_#61pt2%KqO z_eN*UM$Gx~bmH&BivLAg!^ye-P4 zR+$BrSF_z{?bd=GVmQp1Am)4QE<4P=KhJ}rt^k%*YiA1ORf4``tSf?>#+cOQ!gmPU z5k$U)&o-135$pHLAT(VCVJ(Dab7y zipJ~hZ(r@@{$A_Am*5mt?#c$pXpXDo@ zd?G};<$vf{o@Ld%a;jwN(5w3RcT^AgX&c;Ouw%WvS_4w!d{{##y&j&r^mu`B+MQA8T#^4u0WOgI|#Yx-Pb&$mYN#B z!QkD*9dLR(qQ_bg71d*o6dGloB$09rGCGTu>!{8YHq6yCX@Uvb6EtfO3|c-MQ-U^{ z>|^2>cRYQ%8Aq(vWKv!3`FKix@PyiFo3ZRXG=|01c^^=}Z|=t1XTw6_pib%U4Fh(b7*cudIeJNl}B2o2O^YDxf>(lZBu6yQ~T!VY)c!FSuJ?rAgcG4N*gIg8XVn+L2-md zUmvjIokNjypA%~DkjE1vf-`JOxGnG%=uk%61%bh6Q1f@ufe1<{G@_UL^uvI>1`pWn z{3kxz-*>}PD4uvK6B2C7ChdMBDd#}3X=&hvkt;&|V=*y#+ME- zDre+UK*Jl+Z_&phc&c=%?@S97*ZD#w90Nyq>z;7y^7^#Zf{PU%+3XFd)g}K#h$rom z&Gfc_siE>@RSf-Ca*B#H0s*>!$2D5>zn6ELbTEXJ!fSC(pe)6;M0_=l93Z+?!n)We6D#AwOC z*M2MPaNwFDDp|}Bx|s)twUkA3aY%V&#fUue4*^(L8csz7)|C@V%56!y0nrV*4kULxngw(s>=(IJbf$68g zjGx1veIIFYMwU7cHqIxSeYQIt4VChY#~N5;|3emXbYeDX=dH8!YpMKRsmpBX3G8Yih5BQCU775u%Yrk>M2?;jyN7ls;K50A zneD6q)g`-l>X6_BZK}t;Vy-Im)cVwKL{$vxC;DCBGOw#;CD(6R#@^+3;^X15Slqd-TM4jAN^3XFA6WUkCOga2pRf9KFeJgqK!&arYV!`A}6g) zFxvse={j4Id@|~W&^(MZ)?o`L2#q^snOQtAX)_-)8Ck!msmo4T{Cs^NLhOQqHuluFu^Nih3{pV_P2ct>h#nTCH z$HHjBp+X#u4FILAIS8Loo@GTbJb~W19K+oV7ctC?h16u{aGSE3v!M%jQ6l|hhG@ji zoOpozDv$il-S`uK{x7&VD#l`oh=38AC5{3)GYXT>kvJ?O{P_J%m(t?cOA+0%gpl=T zZ0@SKi7nL4am1&PZlnn8wJT#%VH96M*r2}Q=(PEQ;` z;tRvySJ;2zX`FIh;xqt|rkk(@AzT1tm{aK4XFJDI*DTT=q>Zb3i-h}-y^UI1;4FzCdtu9n^>#Ov&A+u!03EXz7 zCWtUxsQ2V9>R-b-dE(hc@`clIZ_4zGM`Y+q`L^N)vdbnWD?!X&fwR#t0?76JXb?YbKFWazX0U zlhEhej%Qv(;v_yCAImngS6wzqd{_R2&RBxPUfmkxi~7Lh0O^8pzmNSwkx0&}5y++nKnzFmOh?BKWwU=p z^JmtTfB87ygDqaQ86*kdV8%(_!}0v0L@dRJu@$;_B4gj{hOQJ8NwAQNRDuJp zNk4B#^JPF`zUm)luHAFNGb`Iss<^=N<0rYoq z{BY3%&BhgMu}wGS{xGC-rxJSu#A7XH{_R6|S^<070(#?qY;oCHFA`#Q-ws{UW;ucd zo<~^H|9KDulCZ)4K6japNeV#EJo-tAvh=%bQ-7F!k^NyCubu^m^zzMVznt_a4X5p#Z*9%}u*XfjVy8>yH-S^1fk)8q z_a020n@CQMQhquk{@6YmE1Yv7;i@xaFYL^6b=2J0zmzz)%Z{s4W=>u2`(<^hOu531 z39}EO-|-zkI%p_75rIYRNZ^50s^(=9W#Y`9?eVLzz(JeS ziL8r-FbYo9*TOhMy%sclKNaq8br=|w8*X;FE91TEX`5=bzvn2J`^s3zdL}dZj2^bJ8_pAzh`nhL^{U`IA4y365}b?xEY_49D`H%$3{C+A;wrxR#Eh759lOCni797KpF#-#xp z(L%AE;mo&GxX5j4a35Q-XJ^uGdR^dytCw|Y%*v?Telp{z6x(N8d7YH>r4Wg37>t3< zMfzx+jzd-d2^HrjNgl4X`d4`WqxT~st~DEKM%7wRnIt(<36Mg=<1z3+{y&v<-rJ;$ktM{svqvgvzpCHh z{xkL0i6}wF%agNrAY!@bL|NO9?pH7|-y98S*c-HMBdQvrQwFLIhNo1KPbtI1NQ_N~ z_b4Fy+xW&)WS^OqqU))5?O51WJ~kR5i1*c_)dwv*qmuXRaXRl`#b98V78H>)waAL| zN=EBRafi~n@1nov@|>hCVp zw@>jt60g~qlRn(wo88t72!O~b$gf~dRa@FoB5q016(&c!UrIq`Vo)!OL7EtZHM#qj z24}KF4`wLJj4Y8M-Hd{{vi1zplby)I{y@#O55yyAEAdEsh;cr~`h)2HsNR1A8tk-rme*=HzVzxNfxnC9}EzM z7`~%yu=b?gX$13Zot(-qZYV0?OIG^_hCqqj=%=y7O78PZu~fS1yjqPqnnNwsZrPR2 zZ|iEZebBtv==Lzwi&D^lg-%X5y4rB~xY0qK9$r4s(Rsm5m=K*XcvAF(0Mord$N~q+ zNCT3ow7Q}~`8=;(y}MR>hj^l=9M1x=iw17uJJGnvnYgKLy^5=}U-T?yt=-gQV7Eyb z=`MEWY#i~jLhr*yH%lxR+QyNW%c?X?HlF<5HhMdAIyrtP-<+89CKnSLgW5On?V~ZI zjxDf?sW9E)04biOFG~?rs>orY#Kye#-}@?BU*0p9vL(}H;%eq6QN<+FaAOmty7sjF z6OWN$&G_=Cu_+Z%91UY%DRc5cM(=>+U+&j`7nXueYyNI)6CgSw6g(>}UhqmzV|f{8b*__qCy*HmQUvy6=s4|(+MmfyBG{(9i{J1a~3}E ziZbv36~yjncwU}5a{%o*93)8>LtZSLC9{RBvYHjdR{!b#D*SoG0sADG-vbufE`6wH zMbC;HQG##4$>zW{*6D~37EBIT>W19?8ho@pOqkeMC@0|_d%`BZtp8+6a#Qx{P*JZ9 z2F8J#zjqqd0C~98j9iG~h=!vks74W6yiUyLF6LNU9w;H*{A~W&gBGda-uYfyiBA|9 zbJlmyu6d!R#Jsh&QIm&N0EKLa_=NrL>1`z}n2=h?k6VD$(7a-T5a{3 zV%&^#h=bUJID_uXA7qsjC>1D{!W^CX_f|@5qT<1_RaC6Bec3FSNA^(|JW#K;h>=dR zc@~J1Vd8`IL0lr$&LCf^|w*8mrl1$&#KMM zWv!ed=TfLn6{v|D8h@Cal?F711C3jwD+Z82cEN5k=Z{Ll^7HZfbzfpw`Uii@pWg@a z1LB|fXf#>T{@-*(HUW-iWC?#FMQFjY&sS`AFKdrzcOo#PM#k+Mg`jGmEmMH>)sRnm z^~ZGdVW?-XZxlInA{6N%tJoHl-U;VKXmOSX96wi_mbE#ABPNVU;%CWBsnN2QZH`W) z%@yM5Kz3c8MQkur8pv!Qx$#pd>`29b&=%(Nk-3U38AM-*!fw(IAe6_qps=`CReUbJ zcm21)tJYp!@{=&uNQ}6{3SDz+gUr`wkZ;c^7T@Vjn!+| zRuYa$>U$QI+@MGw7|^R5W`gb?BG%$?P+K~sSH)KaeWg=qBPZL>HA`|R*p$>P05gv9;n1@@sKg3Aa?7A4KupcXmlWK@3O7KuF?! zJx6+MViQyuSWU2t@4Mg!X8mMgOK?n2RER4t^LUTVbLSak)YpYK@+VHwG<+{eq){{vW zALs(@Jz%^kcMQFo`&=%q3q9P=ddp4y=d5S{M(O+jgOg%o^SxO(Cy?W4vt1voPN!lyTGN} zthu3c*whbxoba&l>yH`i9)g=%PJGl?y1icMv&2mHlTlNA=7h{qJ7;uA4QTL&#B*T< zvn*Cx^nH3Uqy4yz-L`o9z7j^szlu(5o*h0(PYN83EO`2w9?$cWngePi z+J5k$#6!dW{-0kLbL>pnZ|Sn__z4>iH+I`YDJ7r=Sbvg%@^8i6C#uD zsrz(I0cn#Kmt0)QzWGRrf-qyz=j0TV*BSQ?`)L<~a~VP!Az1x zNil#d4glujCh9^QKsBJ%h?dJv-ZNxR*;CKQAmp^@A#z-F)YLVsJ9DbE(Tky;5H0#2 zc%{0bD5}PgSJOJwcc<6LG$&2ez5^kKO$jS+)pxJA;MgG3j5J{wIS;FYB!f(C!oB3$ zj@pS+M2jI)zP_d_ap{OA1DZgx7B@}5(MD7h!{@yf6~BJT8YYobU>jfn0000Q)`v;n!0SW}S;ORa-JLlyJ9|F+=$?J{*^SoHP{PHg#0CHWxUZGvbpQYq2mk>51HyQ&8G5~~ z`uxCxDI2;10C;`>UBDDhJSqSXP2#n@jGouh-z6hQN*Qp3HexK^45rpK|2HsOnDir7n%4%Bw-HZW2<8{rY+oVAO zQD9YdWq?-bbq$p^P+E0E?qd`VAOlP*D`y7a6Gb}9WO^H~0JUZvoa7@X((L5@F!qw5 z1T^vUfo32;(8#oJ94G;6-oi%kMGP;Xj}l|Z0k^=yBR?r2R-+fm*#&99Pw|Tb;pb}$ zDjI`OgIdBzk3j@9*%b~pTc~ncfYIp*P#HCCfknZB4FLsFWWNX?BJ-vEyS~W1Jwai` z*tlDQ6Y3!3C&ZA!3B8zDO#IiRB)nKZWi9|Y4;jl1*p`B}pisu1NtThCBD%%^Edan* z?3XML{G5;gm5@h-4-9xE5g_`*_-oOc6{i(mL+CB~pP^q0pXO_`za)+1i0FLJAvH?Spk$cLAF0zd9xO{VZV!$mZHJ_fa z4`CUOcrkbXJqbS^`jXJOR&rXdM>8nhOW7cok$~RZZ>zWPp-~Qo<|Gs{QA$R!-E4qa zS9=WY!pPH2u)VP{#Kb1hVVB%EZoW%hFTJK>#*2pv6Xt72#mP%&(EvLEgW^JtFN1ZW z66C$)CkY}W*Eq}82N8GK9<4YC}F5HvI zE3mV?$9|8qioL3IOl9`rh|!DFiyO504a~IsQ#MHcq;`q3{l)+Ss;1_lj$R+^Q@kWo z0aIRC8w&`BuhF+j{as$xSk@vAGtXLilk$!STFRYIjGd2_4^#=x(L8!mY5XQn>1``< zV#UdDX?#x>5UN?2 z-@BsxGXbzi``AY!#~6xakd`eplNYn!t8^W6Z|IFsEJ7s>#*$I)i+nG|APJz3K3?Ao zsw?bWSMI_H$B5V^jF0p8cmy?rOTjSc@QnCwmTv*|wz9M(nr+yzz^{%jrH8+#vfYg6 z)EJ1(TK>LMZmJW08x+CILBlW<#vl}ee0tbQ-IuUc@!gdS@z;CJCX~WaglJgkASuLO z<}OAF9(8G@uV*T6yZN=YyD)CKJ8z0{2eU$DldnPrXc$BRa6onQf z9h4Y_FY%M9EA(C^6P+6M%~4nWINP5Ex`_oxR9r1b13qFt6c$17*r#~A>{vYl3P`#S<+SxXvqTfB1e(|!>^-}|EAekO_BBMNi$K-gWPL(DV!|j+TR#7QegEQQQdnDqS z{GTL`g7gz+gJuT`1{?t5D6HUc!^)9&wArpzB_?wj<|y1jgn-uuii6m)$KC6}fz35* zwpe0yywus~N{^mYlvICzVgwIp;8IqDDYr}%2z@8XaF5bz}hg&Iv-|EJU6G4JRj@|aN2=RY^csKJ2d*Il!WSw<*g z02+#kq&gRJh$Z_%P|v1Tc)ajLmn)XcKr%O(Teh^a4c!WNE9)!ht}&vG_4gb0E;_Y6 zm7df@_HN89lXqQmd8o2(5~|Wlv|UNGRByOzPh1r2+bQrO3k%p3L%Y}Au%b?V^#gdf z#Z5l|lK>#EppImJ?z^VFy8>EEsgZ!$Ta!!cNz=Wxxw*j0v7N?fHqbgfNxM&(tg^0Z ztXGnb4}l)BR7R0re|2osQ{o(RD>gD85z{3F$ezlTBK#yxt7bo~`K_(}ySn2ee_{4CE~)=WV>Vf}VHxw^%;ig-J{xnN5E zn_QqB^FvRP-tUMAdo?!L8&u1}nS+bV*55o(IO@CvvR6dcdGV_l)(Y$7u?lgmzGHz`$C&_`Dn!~u43Eb z2bAt7&i^m~J1<75wRxOPM6Qji=YR0e*JzOPP;bhMd5u&Ayh2dOVuvwKF5z~h^yrIx zPKqy6^Z2VWauUGVRc8Wg7rqzd;;Ya<{%vn4?d*sDu8PP{n2YEb8uRZK`hCse`mr#2 z7mOwY)}T$pEI4_~5_&Zn`)_-L7%n+O7)R3S`aS%y zAz&;eCQ3HEsQ$NTuor4gwtrt_nt9DI9tnWoJS|`Wm7RJ>)9XI>^mEFwfin*GB-V^@t9|; zBY{eTFTy?J56QVHobApPaH->VVOc*5@O`J><6QxZ-j=1K!OMI_&I72z;=5%Ee#lb_ z$LZt3!rUW=-mqXsDzO=n4#x|t=u_Q%B5J7%|JW2IZ$&k!N18N4DqO%%CEUaE&D!E$ z$M~GakKOOxdv)yrXQV$;-^zbZ)kvcLHs7sADz+oEm%2U-|9s)TETcI`#kbO1f*IOj z*6%ASJ;B)1Ehirpc=Z_SCA^*C72JNwXT;$3$!S6P_V%B+hGZbnA$;8IadU{ZC6Xr5 zyo*3s4*5L-Pl$(mf4dHEPwUP#Zf%7BsmnROmCydgrQ^zks#!~Is;fdw1g=rJq8$d5 z;UQ@ArklQr6PH*_Gx3k4TjD9yJ7+VSrPwv)A8NW0PC?DoHmmXfr$3&Uy^rO*5j^@<2M20S6AZW zOA*}5QFq7tiVTLfBxM|Ju2WkftgzveLhmyL;g4vAvSMhIKfjE=c~>D}o*z#YSW z?DQ@Fqd-;K7YczEkWzMN&&Le6z8XOm63ZKW(x2p9D;&O?+j?-n*Gf@8Uq!-*4>g7! z6erc{g2Ro>dOK5(G6{YVB*qIO$v>+TDk8~+9(gkBw}dj!hDm)t_6s{DcsTaopZpzj zxC~Q0SPC3?w6G$3I?=SVrH%9s-)z%$Y111!^=KU*vv|CoKFhruFA1!jV5oh`8QuSC zGV1GB!_I;a9^K~yPHu($L?oAEZOi_?Ig)3!+y>W>FzwUUBHzl5UUHN&l+~W2+nJ-@ zW=LfiG3)72i{>9Er@)8%5L(RK7GE2Po2S2~pVtymIWsTMg{6eYMp5OvpNbd1v|jP~ z5z%ZV5UJ# zw2((e(|+5J&8B*ne*|b6QpMkXho>d9iJg}e=D*1OcQq{MK&RWfuj;daXD;rXLk?(+ z1bj!NFKyy4djbZ|UoWtv>SAH*ILz*Bp|eNHtEO>^>Wf=Y(uRZGPJ7(0*INSn`>!~H{qn-+%HO7K zwqe!UZqBh04sRiK1(0eoa~o=>iy#3u*X@_GeiKXmyiP?>;E>Yl9+LX`}+J zfcVn9$UQ~QYUSH#ZNME;$=WaTNXlz4*B7?KWpwFM^6Ge9Z4H0K)DRgsf6`~HFA|XQ z>TVLjh4#I2d9`*q^XS+%o>7UP{pO7Ket%v+)1#Zf#?P*k!Klr=5OaZ%RdZ>p?Wi_+ z#PxV0|8ix+bSb_5H}9X$r|y7#pn;ui%J;|-4e@fpj{BB7+B9>7W=@Df4eCfaj>R&o zw7wP0&8qX;1Qlu;_K*dobZrj%41y*P9~Ay1>;Ox<<`3Q1mXuGSch%tkMFJ zBxMm~M{iy9h1J>cQ7m|~nA=D@maeM#!@d431^z2F6I+2L`0Y!5zZQ+0Tjl-N34RfB z4Cn-CrSnb;Og;3|LUA}bm{C}3I36-z@GWq4=t62de&52nwb=5_?W7qMQajaVMT?2p5O~uk9Kow(oO1F`o3bW^zXe~ zD|r8yN_I;4$KFMN5;P(*Ob*q0r$WgXW)q9+U|%2Hn^Ea>W5)TtwBBg=)89+ss=BY*;hL&Ggw&BPL6}+C4wkn=-O2LI&#h8C{1)y9$ zkI^=r^Te34+?y?XUfVsE@X%<~x?l;ZPj2MGRZ1k? zxS`_lwWuJ0U~Dg-L|_6V0)A#v#h1I5_ElIJ-Tw&Xv(evL+OZ3S@9+Jeb;Sd#KEA(& zqJIOlfJ!sLTJADJ(!cZZmbzt3R=v)+r5+En#eSiEUG_X=$X=7-qWho|1b8QP(|LJI zWvogCnEQk(o%Qni&hI|ebxte*rLQ;P>5Pnb>6C<%+wc`&o3ZddIlR4@_6k1*bf{{I zWXZp8SH+ZNPjs@S8aa|x2(gZJ(RSQNX(^KO?}$u*g!;*f4TV0k^27$YoUTJ|h_2`Z zJFBn!*V=M@>ZOB9KPiF#V9OFO9z=&RG6kVnD=S4zq4R^I$W*%Ig)#8HZ_^+}TV#9q zb(9~!TVC-bKc47!Qb~JZB`bf zp(wbPQu)ieZxYsNA6E1j4jVSs2g6}A&&vIq`9|d-%8F`gVgpO5u6Q*@rJPF#@1zRPjfM?6C>RBn3^hEiThMaw|0U} ztIKWUni0OMYX@&n?(k@v0`L~#xnjROHN|NB@0~MGjw|fy#L?Ho3~E0`p7SMrV)mSW zb+y!^FdixSX{wZL9(Cv4?2tN|ccN?rCAW`2R*dYbs7lj->fT(r$vQWNDHm{ zZ~mx=w^w*Hq8aY}(LN~0(-E2v*Q@}kD`w5Xgsv>#n*eY3*-umS5H?N}xqT!FJTx+) z)QPPPgsBoiOXhn;%{P11|BW}_B~gm^QcmIIhJF0#DG&8?_eBGiH0U)$Ho7dxC+lDc zJ2Ho&4AsRoag2XmopQHE78HDn)a8*#!Wj@!!L$wEq2(YeWo7ej%{}cqWrkef;bDrE zdwg{%?ySXstyJoAK9uj`_l83W{v+X%t4F)*sd#G%JG0Eu?53P5pOZP`a1{EHvC>g5 z-w{f+1yYhzhBkzR!d_6#TGb{IRaQ8(ru@*T7q=e&5r1@e`Nf1IrB0p!iF))QnwSZ= zGlD&dYGC+s=m$D$0x~Zvx>f#8~lRWC_wTlrKy=RCAH>aidCmdkB!N}A6PI_XcgxvKj2JX_g!?d!5160z=FxDH_ zaV~?XOm;3Ze(7fIK~+u%W06FmiV+y53 z8Mu3&9ld8(AArpJtMiFmb&IuMV?3^^*M$I%ff*Y*#i5i^|B>>`hHYF-L{&=r8$6^% zsmoTU!exZ)2hk3g&?Y4zeDumO>@ay{UvW<*A62!kSw~CcQ`W3l&tzIW3~Z(^{vqzy zyBS;RY@-T`8;oWDL(|Knr;OABgy0VYRaGh@>!h&sqBMh*Pu0H$8k!6w3>`MlU}fOW z{MV%Js5LSwYwKx&1ylI@t7HTwa-n=Low0%dEC4-mS~|$t&Xb@Y5jRthrUbS=&JQgf zf7^6G?0H-?p=^k_TzB&M!y*15#*q`DPd32-O;WB^m+IzmY9i$y;WiOr&30^ z2-DvMDNTPvO}6-W-rY;du)bogg_`3>KDB7NYJhf!K-iPi+cP?tL2NN#O&mbl?&=kY9A)!Dxpot?x`j;j)F7?4=jD>bLh88}R(Elu(c?lwD+5W{ z{;ilJA1;2Z87W+85*M4U{wy?baBun^KQK#sv}Q`?XPKK5J8@MpBOgQK!F26kvoPh< z!yo*UULZE4AXf8uAMIF3yO~;v1ak4`4ym5rpFFRU)ZMu|kGu}APS9x7+H&K7F37CU z?~ccqCP>kG^L-rMn&0TG|Mv?oOdcPHx>Iv({l@<^Lv#lzbh5<8SFxT~gEUYHi4R1~ zhvk}5lg8lQLa#-=eo8d<^5aN-{Y}^*!WMG<9_g#t|NSkYx`Xuu4gQ?-T&5fJ{D~P5 z^P(iXW9-~jn`tpm;!a@8H}^<1eCQjR60W5))~H=WPX@7@XhrahkLZR4$;-r-2`n1r z$?~wGgkBZ<1}P0JvIbl-FAKAVAQZRq!1d*dhZplCq}O?Z|ny>xl+m~_^23-BUyI#b5!%jnqptqPO4iU&0$E zF+zmJ2p4o}GuiC5QH_8?SwV3dL6#a;D;16zMTIQv??PrRZ1z#o5xiZVp#()h*eAN%`zBr7fqI={A+;qwUv2*V;c;~z7v^Rq}q4v z3+*_ynhf)_z~TzUMnhC_ueJ`I7MFX}z-4O{MubI;CRLt3ii8$~qJ%1Y)d&T+`0RxA zw)_i``)B5(%U0)27{N#3>*|yH&H^1l!k;tv=GJ)appi`Ve%3*&b`pXfx?yj8|P!_Mjz&d%g;^CeC;b0zO~MNDAX^(nm~_q%|yp+qdE=UvRL+) zG4t(X7`?Fmum6hm1vy(k;9M7>8Fy>d$ccdo-j|URwzX}L{ye5d%6gQ&+Ygll5S-7K zk>K*%AqEKE^RAoUyq!vFtDQ>K3|+4a0}UZl;N|Jk3+qO%@Zc-mo{45pAKLv1DI5(8 z*#vxZM%C-WHW>ai71s(G7~wZZSK5lI*kAt<4X&a4QaBn?D1}^8hU;QC^(-+voMf|` zA4dFh)RQI&*?cB@Go%+CVLgBz`GpJ;$rk@cOFNaZ{B^xyybh0&8zRPJpVZ2wsBla- zL@rqo#?;&Km-1E%W|Kuo263)0{pTnl@;lzW2G~Rgg&V`5JWyj;^E#}R3kuhw{LzJB zfXQj7?&H`7;XktpUY{_q)j_irEWbi2jtfT%i%0nJ2C!d>&TNx21t&1>x%mAI z*?b?;bSk1_Zc16gq){UOt6&L)W0lfIU(Z$=@~_SRfz=QYf3|&6unE;D`ze79m-{Iy zW4nU!CSB?I{|d6IefW&2bKoIQAtqA8p0=r+?;#VFrCOeZAEJG5XS|L`k_H@uu$CTy-i!O9b9<{!W zQ6UZX!L^#r^MX%G_{=H9Drf*q%4tGX4L`JI*=*c+$2Q~iwfF=TV} z*+il6Y0GYBjLUAkQq(yHasBjpgZ^tM%j`DDA3|9k=KbttOJ`1fXCfORa(@au1pX#Z zDY)A0rMipt$LU^ns~Qj6kwM_k%%-9v?2ZZlhC0-w%oYjNyx1XechpfT`HNxr4L*Rb zB$2>~++<5xzgBGEgc3$fno}scKE_86a`-5u^KSKY>{FHm$r%q*{l8#TFi>6X@NXae zFLT?QS~K1sEg!-zoXI!$U_H>Krgg9-3FOZMyZphP!=oY_G{@j6;&*nzJ#M&vy7Mp) z+VicQn-KELs=oh*IZg`DY4vnj_F_FFX7Bz?g!f%FqiGv?fPf36q}|x6vC!KYJ@|wa z5{z1dx^Jldoh_mFP=DwKnj?=E`D>G02Vme+t#X5o@F(~U&cwT?jOj~HAni*Z;S+nJ z_1&E#nk7MB$Mk)zRv~fnF)tb`gIw*!02#k6FbR%=j+qElC2qeDmb|LeRDY6-C+3?t zFB6h4%;W2QG03bOH#IOJ7@n5=(zCR+b?#&JM^9_lF}}D#*j#U_>kjm{s9S` z{;Czbx4QmjM)5tO*gpwZ2HegA;a09^fw(KDFvENGciPpC%0J9(vW*GDlEKIYV`vO_ zkM=OIkQBKdVhZVW#<+o$%}tx9z#2A;TJ~8WXeNRUFdn7p8sb14ULC;rRE(Z^ePUBm zWUXzMd(dw4(@Ws7wI8bf2Z|G|Gpbao&vc;o1n_d@azeP`R{Ogz-)AY^BQ~MU0CJp2 zW>~`vF`jIggKiLnITl?Uti z--Gq>Z1(G8{L!G!K$?4h9CTIJPNHRquNh6oZAreJ4Msc~%Br!wZS*^jW_GPlpj%nb}35I=)j(_cG!kw%n|(vdSP()BPvLe}YJksyim^zhx- zPG4=PPcrN}iG>*=$p~wh0zqDlIYUH2N=ZDBFm{MMJq|*WWtoNp4_GLSgce~T%!O{0 zenmKZ4bncu^Q9?!+cO!`(1sP6qkf`vFXZ>o%RAwENAI>u89cVizt>JJ&w~9G{+_i- z+yji_PrtX1BYRazkk!@(b9%`LVWVWA3RxE#kRzffF1Fsw*;hc?D7{OD&6Eq3k(LRg zDsjiJz`qL})v!P!?R$`5XoiDkihtbOuu$L9GzyMyUtgqHWWejX)cKYA)dl5q`t_h zR7}#(<>Y@ri*xC1!tv>1Bb2VHuB`0d-{_E6Kt)#j3J^MI^S0l(QLbcxC)3 zN|osxR=W!j9g8ag1_)G!M}2wp>$xR_?-RRiz`JrjA5u|kj5rQtvpFcT&`pBe8&oTg zQgA}v_vN+ndIwQ0_h%SA#eBX+TV|24Qej~s7v-}F(DfjNkj?pFg}5zIeq~q)!^(|4 zTh(Y+JP5ad(X6Nna`%3l3XQh1VaSjRZ6%v|T?W-KrxgU80B`3gZgC!gJl2#eD z#w&UnLs=Mf)1~W=w}>VRS7;L=&k^Sm474Khl#%Zf2DtmgFeO;;1FrdhF;Z{d6mUxw z?@ngzP@*Z{C3{QXd54bM`gp(xO8mu~0=OmD+PgQWzM5vP+Xb(mt)CSJ7IsqjOK99J z8k_pPI@lzTNI1g&F`iLPD<+j;?UMsidJHclvVHcFk07NKb!ePg3S$H;xdzrO=qxVX zjD=Z0AHMm;K0GwDNfd-&C&gow>$e6e0b1IpdyvD*``2we;>Q@&>|yl99-Jn@2dKknNG3J zqdiPAF~Tfo7%f-cv?%RoDUmu9ot@%*L(WjZzjS`a(oT+5^c{M?%!H=bq0fzpM?=^Y z_%MLjUyX{2`8qId&CD!hA|}=bqU|_;9f@l~S`@V6^!MQBBk;A#YK@uqNVa~oX==j; z@|YK$}EE|&i7W&c0htfO_fi3o2ii(QD z{IQ`h03V!2JfEd&q6Snu(64HnE~h^65Gn`2^F#0wZDh|0lc%tf3d3{l6Mq>w48xAG zE+@=OrX~j$ppv>6-^xqg`U^{lu{cm?^JT@N2+#cBX4TZl7L7b(K68oFR2v@qokC8v zg35Q<%z9rX<-;H+Sjk`n+6^^pNgbRK73J^K#B_uq93afYrf+p$* z^@_tJ4$z_ccPNmR&|UyuCI+NGkeA%OS)uH*X}T1PjwcmofJpN$HXI$!s{o<5or>SD6+iUB$BI}73eXSSmq8Jc?9d_RB z8B5DG1F|mJKkhJ_v~8N7j8@l?yY^?Hs>+~Z6;Y7tP&K0$9#v% z#+&($6LXgy+ra3$KDHyoU;G5L)wGrt(bwwgDLXq>Zye)eQ>IQ|ZVS=dNZj9Y4h@Z@ z*pw^Si!Q$0p(Nu`Q-7`MuK8k3x#OpnY|k9W!p-4iMY8B{N(nPPyY~CFA7oO=PsC8D zZEu?8N5`H%M07k6og4>WF#vu0u!$^nFf>UU8PdskftV$7ZeZ4buRH>%07C#=Ub2w{ z(o|{ZSP*4CzT;xE(>H70mb;D=RL>bm=-Vo56eSmx??#a%JptkgG+$7BYxmEV8!=NN zSvUq_I@Z}ehdx{$qjc5eKjtF|uoB`vFL~}yh){y|K>j*6)YXTCJL5T>c@kK@-#;af z^3OpBUs%Xnpis+T3?|f|28UXNHfdK4F!)=r1QFb7<$V2z?m@{vJ9pMie`@H7@1DPJ z@9>mtG0Hdqn2-SlYxfC-X?W3scY=_Lodhf>X%nFDVy2@@40enR%-j}*#&>(g-($^N zK2N=j!`)I4FEt%%dXnaDmkOd@vHtYJ^|Yz*$wrmJ>eBj%%Bh=Ex*9zn_kp!Rns)+q z7(^EK$v9FH6%1q&gCPa>sKpdBp-QIC`#K3VWg)x^G7N9qk`55mxBXM=XdC28@^x zI{K?K?zbX0&R^kHMNB-`GhWkR)IU$y5Y0ORXvoutqty?8zwblgQfD8$wu3?0q(bim zRZt616f<>bK3xFc@&7c7Qm5cTsDtqx zUs&p{e>^jZ2q6P^YmVJNQ-b5ZW%J(nQ?e5l-n(v-$Cv@=0;Kt-PZBAtQ4L1lw_Z>& zCiZ2QMp@)lgk%L@qQ<6z$xmKfVtguKpiz}3$-vDNZqwug(ISP9i8Qqc-o|yW<4q%I0E{!=Gi}dtfefNS7ZQ#?A z>etmg_2428Z7X-yS6%e#A?5EI%vadW~r@sVQzw9W>#jgO-KuyX);r1aU~YY!nq0zc+5={_4!+{^5X*CiK^5 zi&KmhM~=el5N=Q}n+!bqwOx{pFEsvy41X!!Qt?iLVXv*TJ~F%jr9bCqLj9+Y*;sjO zq5BtrqJWN_hk24(Z+k9J6F!LLyu+FI3Ux*ecaiod1k$#f$X67ylKvilS$ zm=%5IBy`3@vC^Zn@t09=x`cu)ae?kjnCC-CIV*J!KFe&d=O6KZO>d6a6FLB6UEOIs zQZ;#xGL4d7B_IEYAELzzjWyjJVk&oNQbm{i`qnL0VyYtW=To3Jb>pig@nogNEu4nE z=|wTzF+wHDIt;cG;43=UpFaN(+tJ?`dD7cEQQ_TPwGv&Ek`4{HkOoHp%-9XyjR$gAts75 z7>59*>NN7?UxOPZO{l$GaeLq&s*$8#V_XxXyc=IlIMNqeguld^p}mWWxI!UBZM?IJ zLQ8_C84|gpf>3`3fWtL#VS(r`ZyqDpa{5_?X4k$A!XkM8Ha&}s)wu*EMs z@i+PnK^C?F#U1!CV@W*N^HC(kUjTySlJ^P-zvuF&f(nl@!J>ZHFV%2tjcq0ccw#`T}Lc0%lMqgkjfy8{4Us z7iP>uuCx0tPoc}!(>(-0lBL+Q7j+mt!CmGL5sMY+W!=eO0?hGRkK@cx@~fBi0%@(n z^{%EXpfKnniJY{7+>5~q!!sao%9QGVCyV3I`fe)ICd2N)eplV0!l+4f zwxj*I!Q%#W0P3`u*!|DmXLh~{U9^v2Gx%zM;~dG5Q-z8#bI==6nXnF0yYP;Glq+76 z43ZS<;M{OIYP5xwt4D)dlsQ9@FP=^Li~j$p=5;Y4Pq+}ORe2b8g((Di}0)wgbuz(^;=xannSxb^Kjxr6GdC;Sf&L zv5+0l@h2LzjT``eoM_2#83$W({hJiajU!_p^ z8PZD760iU|7}>0<`@j-Ti!!Ovt`80+oei)v^0OfCA1byiFa6ScpzWw}l zUAzX^C6O8+dwlVuxed+os=^~?1iXdO*QdQ(HNBG)PsIYv3T`-eeVFx?sO~VROy=#% zy4CYEw69!!2>f)g#SEssQp1E;pd!9QoK`WRRoQf~(M&Yhme<)lH>}9G)`21c2>Xf$;Ob#-N7xonBktgrA2(U9~3Dv!B`A7q3j4uVRe_ zHs3+tr&FY2ckv0Z+RGEG@h!JXfuKjVhm@2fUDwzy}jnxXX*BtNJE;1lY8BBm}j~^ zGISA?6I{uAl!5IIbR}}dv{U!vKt$3Z#IZ3=OlF4o((;YmQG?<>WbwB+6GbIzhKNE3 z#+?1HMoJIZr^L?0Gjbk`-ue{vvEgr++w{(PZ_-Lt!&O;!!)!cl4@|i@I-GvQD^x5g zzITaA6Pw~iWn&iyTXEAJ{ycbf?*E6#!+WkeIM#Pb!PJfWqY3=@XQ8q0q$L}+j$6>VWnDwurQ0e?m}J%=737nBKTK(Q^n{aue#1|>Y38WLfetuolQgk0HZl?=Evl`cyDh6?8y zr2rBw;Sh^IpYz)c#DzLux86yfmWs@z`?moft#>9K`djZ%Q#j0EA_26zPz@f){MPGQNi4*JA5J8=QM%`mv2rHH zM{{r^B{)$@Uu_F~hch_(l%o%3QO5K!^}@xZ59>jyLl5tmAg6r`cEosrVVkX*J(D&k zT14=sdSbj554o_ygT+!GvGbpDhzU={97z$2`4e@IkJP5G#_?7K=Hs^mVAr>cX&@yG z?Em1B3SkYlJtq?dldQ;Q4J4H6d1YB!Pq~M_uTXh{y)hfwXG_R*2;s}sN%CphTuk&L zYN@0hWKS7%#C}D^p#7Xx0fle+g`uwGJ)hx#tu(OJvT-sYqY#e?u>%aB<};SsmtSFU z;qcvic^uR1E4Po}t;SOFSJURzyE6p7e8FRd{?C*(DN z38R_MF!A@$r>W7ATs6MOk0-a_`*1eMAi% z!i%J*YH~wE0V+r0MWkLTnk}JiQaGW>-DhH@ncZV;p2U&7=!LUKgyv>orFFTmoYEv9 zPbbCCb`L}XE@3m4E;B#X#=a}!6~c`1kX~jdgB!K|MA%A?L0-+#{aB zy!Wr`4+%DAcxaCdavk{PB+mLiy}vLObsq5lZN1nlgKUQNoM=-~CX{vWtq)ta*a)ha z(}g8+q7RVtxLHNK`X4s^vu6K?270zLnPa#UfRR=A|Gp%S!YIzyQs(C2SvvFe z^%ahcjLb1>Sz*rBC=Y!=ZL2KOVQ;Lf3-Mt37xQHLACLbrl(mY>gt3cs9{NZ)gc{w4 zZ!S%}MHtH9fW8rY=yvY45FMEy{2zh;Nrhps8@Ns!!-93)AH%eP&ayoc?}unL+YrQA zBGoMWzc$#IqWaJ@`DijW$lJJ59B&)eVqJm%>+G4!@X-HjC5xncc98#Z{vWU`8{|Jh zG_UsrmoGSzg)xbVyre<<`Xww%oOSC}`_4i4i-V=b#csVdyb7|c_Avw5-S&Z*F6*6G zSl)V%^h$J42>S#thl1!fcY{uK0I+~ni?#`ec{u<;%!TUGUisGXA=$loKKg3g-QU|= zy=mlwDK!l=m+@=T_QVdeM&~y1UZ850+J*n;aozWYu>Z8Pse*$Ey)-Lg^@|F66e&i+ z3{1xo%D-EFSA`EM=VM`BqSgUxq;kUL2+4|D>GrU|+sRY(?#d52lguazxd>k!L1(^# zBk>p>?AADdXWje(qWza|WRk-$i z?94o8o^y!t6E=VU`J;)5h^TnuJ01b|nWm)tTwAOD14gKaN=8Mci;azaB{DlV_r}`# z<<_T(P{jO%1H!A`N{FI`TS7Bn%Xxg$Nipe8tYA2FWd8%rQttVclY>Kfax#I9e{xL? z=gis~7ozco2R;ElVt}~5#PC;oo6F0~_kn?{9G+?p`udp$hK6oI9cum_9*tSq*={if zKP;91HWWtx3^5Kv_u`_#8zZACr-7g00(#FQrKo_TZHgXnl z%YcrhCB5$(heaG;?eqJNY_4db=tzjPzGcu+^!42GQo~U#>Q`$4Gj(TsA@%rb>6(GSXfxV?O;na z<1XLZ+xu7JX1Ea=CMNNwK1WjwV}GD;J0CwweD|BPoA^E$`u_Mp;~DN@rvB!QIp|3{ z3yWBOivYm{pxJua#rsR~gF%YUr*}*#8gyYJDkVBo@|V4sOtYu1?Auz*aM! z(LkSSrMpreUClaU$tgKY_h*&8)Dy;(&TCPjc=vzu$jWshZ7ERnx6anzw5lyo2#sXa zX1eXIB*qsTC-c4>6{D_pegjy>oi3}QMD?gpU>N<32&^FtgpX4KGnafzfd0E7j6Al@ z0#2W%JUGn^x0^EIIxn@bCt|X;@8Kn~2r7wvm9Lhzc8EpS6Beac(6ewulfrODOWjYu zJM}5jO%k8G%-eT|HQQMOkJ=Ja9)+r@Y4nr|9!C=MU!*^F=nM#J9<%WH6!8lRe!IPr zKz$0_8OfwiOG`7^-fX&_)>uPHpy>a;8+u8E6mN$0bn+GteB`t~Fg~8Hg-FD~!NDsm zAb&(bPKQpIvb9mAfB;3@d_3I|QOW@;(G?w~>nDgi(Uh)`d(iYqD;OGP2mX4ksF;|M zL8kx33~2d@5;^|0DM@0N3Kcz~gY7Ep_I%N1xuq^SF_nEg?O4U25Amn#Z`?Z`pP22q z=GBk=`2#7FNt3)zG9P&GsD*85IuAyhtzI3mzu;jt()!73bv^D^dXom9UwGJhU!FIsqrNmXlAeHlrW&2-~xsPv|B1OnU~V zT#b#5wTzA9$X)s|P{Tb9-hE={V(FVOFIRFy$)BH_V{+RaOKWKXm-(BmbH{raN~GC5 zjw?-`S?>h)XWqx9rGIRH9e_$fFVn-o!lPf`;KmJmANjV%h{N*NW_d{6_JXZnqeuQg za(8Kc;T?UHfFm&p$!GkFZUqyQJopE;Ao9+5VuvG%M#6Qm{hZV;gL1$-S;S%pfI!3wWWTu)s`MOVCDjp5{^JbWnSWp^)mxVkql=q4*f zu*9N+qJ@Q1igu2WQeGa5N zudJNSc&e|cy-nB|VNhM#6PdL%W7Z+*Y>`uYTtJOvh7gf(Ox4?r;cyEMnQs9&u>g2p zOt4^0f$L@GMJ$0$v5`h?>^2GoH&@q^mKMF;*6-gbP9{ikfa`$u&q6cYbLA))_?Mzh zh$OF`)~Y9@*R%9=uRh)QFI-gbzwa9{@@aR-y?Xv*V>ms}>A%Xgi;YFlzK3y*B?rie zO-mZf5m#Ydtu2!HvLRK{*ht(HFWoc&WCyB${R0j0jEuoFNu8VRP%~b9*`oe%P%+=} zC`x0^#>2)-O{*`LTx}U-Z0IMmex#ydX6FTTgp34lDd%v+&G4*~#^m{_m&f_e) z6Y_Dh6@Vg^?2xSI%Zo5sYzse|h|usjBrv4(cg$mDZFAD@os>L)2)+Xnb8h|AfGONu zLvpS3@hlcUEyR5&k&4%$MN{p`)hV$8SG5Q1QjIa#2;x!jrPv+TFG%pxLpj9$_{KCL6!LJ9n}-7YHYC#-DC z&PszAt*$;3%s6#F7Zyg(0lXLbwYBBROlNuSN5s(L!onnBsTr7tX!=h`D7J9}rF zhi|KxA34oI%PC`np(Sz^m6dL=smaN%wG;}%>+!&fySuxk)rozN!Y}DVn3PCA{UxY_ z8ZKV^9h4Ci@W4-!u)6bpYVMmhDd%_bk`?jYtGAjaiQglhxw3H~XOKQO)o!aG?C!p6HMc2 z!{}^vx(k&U$Tjh>Nh&_b7~pdhdrADTCvyA{{e~vZJIthtpHUN^y5$w2-0PDK4bxx? z+Pri94D$^Hn-6d1!(KKojEo$O>^!aAJ_a?I@8e8-Ew&*A1JDa`n{KOA0kSSy*YEk< zE5H~E2%!7lQJY$<(!_UEV#KB_OnnB*$dE1U0)5tZ4i4<1Pty;-^ds#`0O>Z#5wh*K zZ&)vV#SzQqYRPFQa9`qL^d6c7FqX3y)6NL4+)-vAxRYsr+0OfZar{&38*cWRW*R6M zWXE9m+UD}3a>w-MPlNTc*v)BDofL!WkEl@shjFiZ_BbRDpS)td+WHW{(d~eE1h60u zL@XzFdo$9b3D`fVT~^A7G-sPLLcuvTMAoHimYJ0mt&EDRz_d=&j_JM?v^&k>C~dvI zadQmY5IgQ$?k3DHe#jf@@OuUx{Jx)o(gTP4$5nTWK1&Bo;Mya{6RlriA37U_p&}GlEOMq|jl3BTqh&tlIiMudVaR;t=cgjY&_p=wiG*{=#l@wK3X{DsU|9bm z9A6QYp*847#K_19h&aoI(j&WO&!`$_e>1e{AI^N<+D$NBr(+ftGThu{?GuY#Q)Vtx ze@5DN!h^J!cF?_=L&D9|WQ|LW9z7qEEb5*2T$AdYVLn|ftuHY@>(j2DsC@eN3>U z+p&8aBYAc8 zk2D)`B2ok^8p!5*A>pv3HamYbFcfsgwhMLEiN}(uF=_3SBGK8e+U&qU6N`NC#6DMiSAqUNDL2AIs>P33!^i^YmyYzdbzY(IlX$ZzW_91@~c{3q51Pv2J5#n*=0oA~+$7lkE@J zot>iv2}t~$Ak9*r^DD{_qBEG3JaNdHJhLCC zG_qv zVba>qEU9IWHdxbFlc#pUqFlUr~GBeYGRKpY^Y?$|9u}7jwYB0 zN66WJ6PqaR)5Ee;b0k4rfJc|9)2e6l4-D*2OnRI{=WInVXQ|U``=eUKRwy~pmSK_7 z&LWDdKSCV7$l93!v4Jd3(z^>G{2U;DlKLOK5>gR8v}=(SdCZ`G`HsvN;vyFZTOl@x z{B$5tv$P^G*i0w0-2I1eOEu(&aG8$Wh6b1@Ua+Y97Eo#z?<&M*h!O%EX2Hj4E|XL% z+7Q6Kc&`GEk17)8UjoJ3-!^pvUuDO@U&jpMz^^m{htEf)xncCXB;iK-+)A=iUmJsE zM54W2#{{@bf0?ir!R0U%REShR<_v3qpQvTPM_iXO1(3*pa=?h_c0q*;MEw2z-xb>b zm;C#de{zTbAykqj;*eV^nHzJa^Y`?v(=n;FLrUD=X6Y;}$1%It(8383Tw${C$}l8p z=>^m9{i=)_M(LF8AFgx^{~jdB=xBnyiYi`)k8esr8uWA8{g_3g)JoH3z|i z1cDhHgz+tRT5`#5(C&V7Ev9(eY7ArPoi@qeXpO=}~P z$RAHin3t6;(FJop8Ew%}>WH-Djr@Uk3mmwH65G4+>o;~B5&pJP(1K|8%-0XxSf;Pv zE74Rs5D?(e+r5`^r?;(edeDz*ULVVE_eCdwY$K<7Rup~6Hh|zSf74u&$*$=*!)fE* z(lcOTDfB@#*%^6`9TQ&bNl(#)(XJ;_A%l@9lA*E$5Hv6n=w~rvgc&@oh=2o(fNr8M zYXBc@V6^nCj~q}lYIF5JrJ9rvFW2$@5k|mKg3+SGpZZ7me^1H6HvUvBZ#`pxZFQ`S zUIHT+{*m|o${T7AvI_I3niv(G+RnyiSwYI3A6J&3oK{68*XKHB^%;Ofo#A zAHPxikhszaz>v6GnpbAA-@JYBg6pU=#_vM9U}NLs#+_(Ua^0_r@$W;8^3gyPb&{MN zmRt1UR7g}>FPU%Rynj=4#!%h|TbIKJd3dGbchNzi(Q0Lug&G6#@$m;YH}f^MwbcgI z-82njvc=SZq`*$+%9-eW$lmHIfmF+@we<$B$N(psfCFR@KaEC(PJKuSB7Bw{gYZ!o&h0O3Y!nKRW6J>IM5h9SILF8HUo(1odcMN~ zYf<7w!d&J*hT}XEvV(DcvY+=^iURwoo;^OaxAdg+jvM`-{>g{|3Uzu`dG`T{m6dGv zg4v44{15(8^&pe5UW;Il7LR}j{^k?wA03Y^sP3@u!MxP`{>5qu5ywjA?J)oLD=~sB z9@xwaWv%{jdJ%EbHEZurBPi z_l@lNCv2X#)mnZ&UdO3BO?<-9*h3hqZnX%{bp*`j?a$JWi5!Y9&SBoZX zSgjY?2Zqpy?@I3*v*e34MTLdy90cvpZ}``aVREzaxZ*c< z>~(Z&ES#TvZkbJxtA?uon;-{1+yyJv&LI|+=a299{&A1)GQ&$5WT?}_x^sY=O(&$G z^X!oFWHf)-#LB9whvL}e;cZ)OI5!%;EkO*u@gv8H2stB(k^lMUVzJ#pP{s7kn>YA6 zzfQq27k-ZNL3%#;cA_Ap4vi~ffBz+UjLUNOfzbSb zA7LxI6ly(4_V()Xcccz^T=JmW*|*0;*)h|TbANx=57&35SOA=xgyT!!jqFipa^j@4 znNGFzm?u%!gK{w?M(+Rt=Pt9=+1c<@O|4*@t0X)Of~ZjTYIeeT&o>9*`4eU8HD8;4 zwE~p^gaA|rDQ^1mQZ1IQ4JU`x?B+$`bTr3}#db=+2sZ%gFi+;|a1DHnFgROVu6u(! zWaDB)VNCq&9g&;1O|>1gu7-sfX?b>L1~^8nQ~r|(ilff~h|<-X?6^_6|HXw3ZeoO# zJG^2bNP-)L0~fhw(v>t_>Wtd_(WU(;Av7oSCyi_I!>8edEmvIY&<o<*U3=QXQNa^>+ zxXgTlPgJ}B;sK2{zcD6N8}ZLo#p}O1gNrNgY1h%_%m^EJ!epc$0wsAtSm4~;T#H5n z`B04yJhsk7OiEIVpO!e!F)>fX5si2=iHax7KTz`VhlGS2C(1Wj!Bme~2ZzBN@Gb+{EFaQZIP^TI(=6rq5P4XDO@kne4GwJZa^ljI zHz#_w|2EOvXDDK+i6?-(G2d%~!m9XvUUoW^%X;oqOP#0ml1zVM+I~PsQH_h(rHYZ> zA4|aga}Z4^WhX8Pk~U7Xb0Dh3@@`K1^@2^pO?CV1Vlg5PDhLPs@tTuZw^fn#VUnID z`D=nlF3r<0WDCW)3+`zEnl8nuvzSrNK=BK|TUhF#c4IlLdODxftl<*^AlesydtY|8 zvf*dG6n{wDSjN%D9PiSO0KrSIF~U(05&f{^ePEpw7&lXHUyiF9_KSq}wWZU6q0ZeMGy)>mWCr3f)ge}r znAx}elVjZOyMi7sVjhj&Hb|sOsG>n&?*|Iui#g~>kwSjKrGKUU%uJAX4vm;C}a4H0qYV5B6h$3M!r5F9PE$KQ)n}R zJD6qzBoTC8`A|hW8~jGtAw~)Xov`o2J$a0A(9KO!obx-+CIrX?^~m7d@fv?WBUhEl z_g8@M{R>rn6m5dRWB?Aq&BBpWN(v5@#E*D_y%#-6qwkbtZgds3u3l4K^kigc?*PIP z`-1PnWPti|C>lQCCzcBCIcOy=A@v_ZC1hBkl}+WD9{Z=z0luW*3^>tfMm8T~HaM=LmyNQR1 z2q^?hY1X>^W0jW_YYC>3J-dTS1Yc*evoc@x0UvCyv0@Rg9YDLjzKL&sQ&XdwA=?2Z zk?Ij)mR(R;UZhy~wC8%^w49Op;}gk#eu!f~nXR2RXZmPQ()g~AkQA|5jLYOIS&l;x zwHd65s%*+G02lS}6hc!m?{h3AsYNQ)zqmTEoq>DTaQ>PRvAC3yLy?utorN%#I)w|` zBMyo|qXe`vNB;<9{-<64s>7?|q#f_z7mN?zSqT8fB8xO83xuqZ#jI55>h6(=D1AmC z^cDeNAz>jzt@z_##$gvWau9EAw=B+4kY{I`2`a`i$mDeeki}!bXmL?@LwEKbdND7| zla?F#9?XyZb*Cb`!FOgaaVLIuC@zr>$Zo0r{Dh3}zWr*kN?zWGwFS=z)A+S;`ojPz z|I;9A3?X>ra-6b%aKyh~^g8gmzen~5EeFD~LBY%7>g%3SY`m6*W7St3fzN$cjfPaz zFJ7Eot1nSsUthcFR%0^9whl! zm117d1ChPSYq#k2c>T@`#YKbK#>lS0*CDM6|>IWXR(m{&HTP;MR?TZSD-4*z9CTA zK4-&ER8*)oRjNt+){fDCDfj!VQ}f>f;0UizFSBeI{i;R6XL2B9awa4UVu526qu6dZ zn)?^694mv-sQ#Y(3xEFp{|5s5@0mzZX*8zoHd)v}uF;kaH*__bM3Zi>{~}X31SyEy z86?<%3#@A@)e6f^dTC2jHX-pvni8Y`C9krhDZK#=^ITO8%~!+tC8o_pRYXmO+J6un;ey%cAVct0G6~K&#ecH+ z_EjZNEkDg!4fyhMzr)A9$`_ZQLXW$-W#VUdwho$x0% ze*>f3rADtI@vJ0poc%BSbn%u-4&z4M_WXX>x9Kvsf2o)>a>o7`*y-p0FuvOOEaxG& zA^A!fNd2Yq7ve`_;>mFSYP$Et^G>nTdXds$2Jfd*KhPk^TP>jwv6bslErwDTSSZ;m zZEH%`>6y@$4A=wF4KNlnYVUKck_8fFnt*g;j7&yHW8!WmUSnS4BdZlm)W#bVB}yrb z*kJgo4{G@i*2RK<=~@8aE_Zi#mvY1tjKEzwB}X7eX%vO_G_LF;Y4Ouc!5{d$K5UbK z%C_r7|9BrpL5)D9EBX!r6y2N^cw37-Y!ZeMl|1OUKr~Ri64q~;QgWO8_B8)EkccHn zTk_6bFzEBV2Fz>qu@-hYKc6A$yrJW=H#95|E)Y3g0*v^ZIXl5RWaUxkC2=EnTF@^2 z2D_q`s_rslxlV3K7-PjFTid1!bz9WSre8?fF^9?J%Mg&na3 z@j3eUZ{M;6O+Q0Je8(y*W>!|PadC0)@?5^)5##7)pxrR%8$w?S3X+tTmZrXiCL<(? zi;Ig4+WmyeBI00!ksu@WJy!Day=_$+8qgl6=FNOkE^ZzHmU?U)(Jy^#)Ni9>wzv7w zsq>{=2E8IhJ&8JAA=a3D4^Qe(!1voidri{tb>%x3f+MBz*p>DPUrkr7$E8|3WA2?Q z4b1A*>(^MRYPYwya8zF78i`FM<{&?Puj_y|AL&6Mlql)Uy{Nd7!)mT96B{2NURztM z^Da3#C9FTG*hO)CWW8>{=i-LPs6`LjxId?fJ9(-WK8S(tF?l(CmuG3VaZE}u8TS3$8d^fspv~99`LCx=eA`5M3&O2m z7IDCiom7TRPCcc=@?TSKdU<$bZwRS1ZR$2}KVZIBw(cWiW@ne1V8r}bYZS~uYuJnH zm`izb;vXWW)Xm57$S;Trk0jHDdrwC{;r~q4moGu9NnQ37Y*D$Mc-Z2zukiBwlu(%& zZ|3^(al(%uKkR1s2sYU#K%s7?3w0j%f@E`5eRu5+1wLW0CoYmzEKip$h} zPSf^Z2E@~%?eBkbJ^k$s@z03hRCd;2Q2cFT2$wSkqob0^8VNCPp=O}nv+SiR7C5{l z;G(bxOp*`nEX&(mM~#47i3*PaboT1U`S2Kv zFF8$ElebnUXX1hj&ELXk`}G%W@H=d{eDCja>tpG7e@3Tn=~xTt29{&aVJXr%+dUc+ z=vq!0xBc0SlUx)n1Sps#BKz!OmgGzKqw&_)hk32R@Lu)z*|9a5OIq+P1&1^s;4m(V zUSd1Y^ZM259i?6-0X~m+Dt3>Z7^^)Y7P9O_J2~fSw&u3~Hpl+t zc)S-{mYz;dlUXvD;#nez_BrG03*3trjtY-*vk{Jmzp34!*Rlq8FAkO)Dd0>W-o`rk z7CjvQy-v8U7z|nvzR&+$366heLr$))uI`kFGcT z$9k^)O?Dq|m-jI+is8#wtAN($b>Q1?=Hj~_5pf(Y@LC5EjU#q=n)E3cKQRiAyJ2Nn zsOz3&HlDZRuA;KGeDurMMPrJCGMPme*B^Nl)B&pj|o4qHDM`IsLkQwvTuQfon|Skj3) zJ#|?CzAiTjL0PD4ct?5y+xcbJzQQR?RS1#_EW-Du>l){(RZ zchQ=Cu(;fnqjFto4=_Uf!hs25X7nHPIT{>{@qWWXTMAoRO%R_ws!$=-hRU{r8XFrK z7I|xQOKce)Z%S;c2%uT@lQUDJEtOoF>gA~OJrcDgAdiN(PuzYNoB*v{zoS zhsT>_>v*=9T2;AGax$#&n1nLuaK+LC`f~vhjGZ|<@izIiNVH*yOk=zCbDoj4ZQm%A zvLGLXCx<>)gQaBHzz8`BgO8^h_;b_KTjb>A2SV~iljVV~E1)LNmYyj5>t)RG?{U`F zy1WeyiOqF`HTrsb>k{{lN$^>Ug-WdFh!jNW;INgEi1A&2-+5tW`R6ZLl9{KV^&OEh zjNJ?b(2L6Bmttl#iP`DLuJ!bsl0k8)z-Z65k3t%)rO|k7^*PE?8VL1oOpUyC-yYE6<>*?0lGv{iahlM5h6nt3&vpoHlj$PIsPac!yRbhob zr4u^rByNt5jz;wB1)azs1Wg@^Y22}Waw6nz?CFgj0wvs1fr}T=C|Ap-3TpbK$h75U zqqT%vp*LW(eJm^-EhfueH_ch^x4KB6ZDJm%{2?R1E$sCC2l7*ycjJc+$+BH-fW{6+ zKE9ybD6ndp8UR|$n-rB*WCl-aAVB{1WfwMBmua#roGe!-I~GRC(@K7T z)Lh4oM~bN=ere&c*SS6ASF&_o!i4Cd)G{m=h#rN4Innt*Gz~BW2E!gNl-SR%$qNi> zTYtLqTfHXzejrdz1=b?MhswsMO~iQRRJQj>+KrxOczUn6_^_9jrFY$q?L; z$hf%h2$p`moLjSj;4B9GVt^n`t59=1jpB)NvakS+*tyJ{oNyK>4M`((6hF$nn~UMF z-g*8pw;3C=t!k8!)18C}0tdL6-Q)7#Qu-qcQ|*AcOrJ(`CG&|OXzo=U$jHbDRQc?} z>qpA!ZZ;ii6Ql=sgO+$?%LBMkj-FHL{(%Dr`8C@s^qdA?apWWSU$@UUwHshWN(SkD zMtdeyqt)s4sbB;EeaHj_{q{}IX{)QD#S52r+0G{%#8RrB!%7A?3c9)<#~W{T3yD&J zP{cQ~nDm>_B48W$4ue;++50&)HT9HcT5$0wL$n3Ox^xEyEk8vwJmNushrr8{58cvZ z;jUmnzk)JVFG}A2uF(scJ~zjM&-tst-Ce?cBxsC8EezCuf=5s@r^thLP@+*hm`UXi z6SHj0#a?Q#Z4o5&lZF@o;{We!sH7z(!rAg9_)A4Hd~Ol-FG)i-jcHmd?W~#z_Y9jK zml^#vDn#;nWBv>tsH$O6j;Ig*J^WCBya`q0Af}cw?yYVa(M>?pS&hgrf^RWw+kH1vIU`QXtn>onf<>bU`({a2i+8+QeN3D@)W!>HTNQhs= zo%#BP{m$efKCN>6t+Pq*ltk ztlb}s58%>)1}Iq4X(klXv*gqEFd)DfFMF{=-Da@7@$gMngToNEs33%ES7OyIb0i?ha}-;lf9+ zXzL~yNp>q2u)NTrf)Oo2Ut(WQ*kLGAdq#EGLioq~{)sYxJ5ozyH4;mcd6u4K;QgWq zK2DL(*VYoxMB(t5U7TekveA{}s~w$M~WL#h-P2Os=!|$m|JO0R%>z;jP1Ry zZ|p!kSch?nB>_xUIa(wDxvU7m3v)-B)?(m+%&tg;e7R zq^-!LvF?LJq&F7mFzhmWSDf?eAJarl9%M^7&;QzAf{9@idjXI{mKrm((M3xrp5&)C zCVUg&BOLid50_5upIuc_M$jL#^h&!D`sOmG<1yDT4`Rk*3puoVR?vU@w^$gc^^=D2 zWLRiO`>a03T1*%bk^h=8LawjM<9Iip8*+^N$=**~wszD0yVKj>hw_c3NYjN;4j=!2 z5dVLPf3f}s-oK!KQ!KPt{l~Ab%*=HE73qKZ|4sa3H7NWasBg-A<^x3sSV5`zU3QywOyZp@qC5_ zvk=wHN%dJkNdd?n_rh8d_s16_k{v|ZONKvJ7x|GtIub7M;KvS*9py`Sd1_|OG=HWJ z*ybmk?SWursLGheSro0gL~O}DFW6k<3-<8@pNlF2VB-5Bi-%2)hY`6O9b+bQNvFGR zI;P(!Q^g_zdY;VuY*8Io$;g;n!tWuY(FL526yt(*%^hp9mWEY(5o(qcIpk|kMZvPq zn_SqS2y!r)Ed;wOG@dLT`{f@BxGMlW1b}<54S}m$AWnZ1s*xq+(pzlc$xOlIg7P>{ z3}R-0T@WnMY`}4P{|G>dMVEX=?9K2{Db>M|=oSlM#)3=S@uXjj(A|m_$&c#r-6ypG*A|1Zc_vxVP zQYj3EqHbF-h6w)EWMQx@V*d#=1PCU=h*~=QSZqlpd?Zd=Hl7!385$xWf~61~a%mX_ z2GQlT9|F^3obj#TT#M=V0@I@WFK|S_Pwy*?I5B}mONUtqsC#BvI1t@x-O@>0c&w?; zr35I}`prbSZ2sZqc;_CFr1iTB1~^t9bMN8ouu{yh0#7VeEbvawxGe4yDJjH3zd(o} zPA325dO^YJ-R{S>!27<>PVW>ZP_RMdV19`0IvFSAgp|F@Oylgf?S0R6zT{SKWxA`A zg$ySW7jFO-J+*Q;ebVcp>Wel@;9SN=`P@E;6lqlx6=#!(fR}*X&i)-t+N}6OY}}|* z7_TKEZgB5j0Lf>mKVKVTBE@)U(4C=b?3POM{2rHjVC2B#9oXzS)Oi`v3_ygNYCb@E zG`L;Cru#{UdD^*^Dh%Kunl8U4058^GEf&u))21o+CJBI$a7y~GudX*zCj^nHm;xkx zzP43?tk3$YQk7pu^;-P8^exq6|6C=-Po5@4LnD!|KBlfV)UU33JG$B`^hRIa{Gm9N z7Mo9oT%#lvC@tKpnyP|9EWPc>`cpFri^t=u!&mpOEo4!)lVJtqm==AUh{@6MAGh>M z5Lj*V&AkH0NBGXwehYzva)gilRs;0?4%_F>s~YDpQ}fe5lB1~O`1oTe)#^7edcTI# z`3t{&Tg)DV7SpGV8j|J`4cyqpL(9l=rT=~ugu zz^wq!fYa6v?VT~~{J_;-JsRGDa?8-BCc(YrP|RD6AH{WbJXY)()IX_|aWAp6=cki* zZ5?tbgk~ZTsj0B+?Ci`p`{FbyUFCvzxiU#OjhI*1O*?Wn$ropBdcv{I$MeL=kK26C zW#CMM-vhR|$n zZG}ecO40=cP-Htpk$(+1zwN926X@>F|2NLnRGE6ja5`7Sj~{YYZ$<6rYI@J*k1te2 z&x)FzmipQ+>I3dQBVTL_d@}y-Y3C1z)wFDEY+@fTz4bD3iQkQ?nT;V$OFo{tF&h|F zwfZx@vbev$e-yIvK36`pva$;8x7aVf)X1J)rYS zPDNb{40n{ib+~Rv!&``yKov=S-+g$Nk=pUMrPR^wr?>kwx5252X(`pGrg!_UEs`vQ z47B87Q;$pvB9R#NJG6h~XqIbs*8hC)UrcdYd%q~tsfo}vFDLgoFNiZE10=lof`Gt= z@bJ|6=f+8e_F{@P@%FU*+uz0vEca2M(W!>CVS(MTh(FY1W(NnT;`Y9NmGDS@Q-f>a zvUWZ%e5XuSo1OD_Zgi@+W@D6C?$i_&(GCv}#~x+I=W)`E+pMCl@ecdZ6>=ay{vMWd zA$lZCqvm8liCD^4BxYd$n=g^!!Em=LuGgxngEwt$7RaH}Qs+O}xFW8vkBd1`utQMw zv~Um^izb$r#poFlY%;U*!qeFRzwhfz!_SU``j`<1&>i5k(L}^A#Co@iTa7|{-47$k zcu0{bc`PWpeGq?tBbv9SrU@HApvrk(mg}0jx+G4o)x!**S5e3Y=nI2lMGLB3;O&Q9 zpzu^4fxDpbSoyZLh}50sY~vx)V}CX(G6(GP;f#!wj(tm0E(NR#J~>0*82;ja{pdpJ zG1I0qyg&NW18#Sn?+lXk&u@sd}ydZd%E=J0=C9LV~zGJw371MbK4%`%$ht`%eu$qY{x6A*aYHclbi!d9y#E81;hT zQGlO6v{WTMJX^^5bDR*x8b-shI@a#<&58nOl0n@B)h6%d@!m`&ZRZH}RusMTin^Mb zGTfo+jrsY`s91z|`C6@S$83kFfGditS+}{=#Uutph#2-Usw?D;5R;N0=abK|2?>Pb#KTC4~Ga{$&AL3;X{ok zvHZ%^nqr-@WDgI?L}whG?6x^Z}rW9I$*>kR`?|0q%)Xza9Xzx`<*L z_D)=SL_GS9Dom>=_}eMucQnBvNE7ZWhpD8bbdkW0N3v-3KxX%1H)q+Xact!5P^j9owY<I6B$j7 zX=h&3av6|1ffQ1b7bm$Yw91!6x=&Hh7Xks4%q~ovD4ns;n5mJI2>;&O`=AIs!oykQ zGe@uj=vZmoUd8upWOgGAf9QyOog~@~o}dU~>-&KY{dhq749ZRVJ+Q2?=6n46H*`xg z!6#Ho#G==&NDoRg7tILw3_>{ae(*;ecxD0xszf&`%Z=XyTPJ$4x}}YwR3EyJ;ZyB8 z1zv>z#HapWfyJFZ)kWV;BT#5NPgdK-(3Rfd!+F!33R4Fh@UE3{A@&+`h~rM8|ePYQLqy_S{>ZGBf=D4CqA z3Z_Yv%lD5(wyo!E)#bOH`TR!hoBK~&c#vGn4~? zB1HuI^wt9ET?pVRmP7e-E`^NKVEW3+#_itwJeEbTfIUVegrY8IP;*9P0jVOnh?l4s z>5b@H>)yRLfjg(PA?L!0NuKTbkp@MkvlREyktq1`MU+*fE7w<7t``WW?!S24)ZODe z2JWK%*Q66ndz8aakeGL(eF&Mkv)GF^%_01i$PNs}89@_sq)%PE(g#2YqHwa`-_-|nImg0blw+&nvrP4h#qN|b5ex5^(N0E=s4 z8FxWrh_o_M2&_V!WtMU#*tX5;bNwTEbwt%TN)z3E@-D{;voExy;oz%&L|4EUOrfLY zP=<0IvgL1YOJ!6v2(dmS9?&)>+YDeL1c?s3MPty21~Huc@f1>r@v<1dr!%1Bkzrs=n3`^!|}(9nw_YJkKX# zwQ|?w%g7APn)8(9Nr>CNp%dmbMvvcH|*GwGmHMvGUh-%;429Ox`W11Wh`Rb&zgB{DFGBdT0Tj;ROpwUc3#Bz z7myLCC1=IMM{!n6vJa4t9(E3gYf)~_sa!4KmCn8k6X4?C&)@N*#tnq{|3B>_YRAs6Cq?{dkx z97qFH-P#SRksw0$H)`Hd#u-SUgQn}MW+_pr0wZ@fH|;@0GC<@7P-CK2Px2+|748~^ zC^RC#-v)6F7(YhhhMkj%+O&Azk&Zi0hrKeA}YUsO+Hwv-|T|J8izs zxM?kF_%aVh3-Nc7#KMPkX;b%SesPOeO*>2?vix|RaiXNMFi7F@RP&8d{Fw)w>IV7w zLtCuY-V$^;2!B!2 zi|n-)(GEByZjxjg(_C{RJ)`9sBJp@$SL(ce^JQp;v)j#h4!O8@Pd6w0QoyJ4t%kR1 zw&#-DbEF_yn>Qo&oNnh%M$_1S`7pVQ7(m#RPt5{*()7-rc~4fn)oB18VS&Phr)sAzb}_Q1BaB^m-S5>D`Da&U2+cHmvc z_85{=3<5z)r%^ip0^1iaCBl%1rem3WZ`SRH!HesP!|h&Zp(v$zW5??Y)c8p?JNtl$ zi{z{LXmSK_Zql<^Fm4TiXya4==D!Vf@X57J~Ng z-*RhbQBY-EXLxwL_MSK}>Hpn<3-pI!Cn91Em~!rUq&{6;)jczpZgK_7uH*f!rYy_< zQtK0n|GyJFaO%H$@JEqjDV=8i|A;yZsJ4P`Z6^c>E`_4Op+Ipf!M#wRc#)#Tolx9E zf#MDYiWR3VTC8|*id%6?DDLj|r{8z)f7c0%ti#O7%$c2G&Yt}~`+tfR{r9XKQbYOw z3bPmeyApDiNj&jMYHf%)F<$J+`n@;DzL^dlzft8VfW=2m!3^42tZ3RE#H3_kvS~CR zWc%qbE%E^7*b5kS-BC4bMBip)qwJ{qcygy7SUgnA;xr*s`(k@i(y5h>)|o^%=-|rn z@gCbZ?dUWna+*`3V{NMo+l+i&K(%d?`;jwXU{VHTvudSe8khHC@v-b)%7Y$jrSWh% zn3ht}@Ah%tNSY>sHv2L8q0T_`0oAiOl1?~3Qh?rq!VhVVo*%a;g1Is;l?Nh)SO`u( zOS;7*z$0G=Of=z-&4)fYzmLq1L=6p`aUftB)6cd&7L%bW*p-!v`=!)&JSM?_Of7QG zn{t9A>?sW%BtkUn_Th}(%+uvT!mB*{aO>B9#tKrJ0|1fWDd*r({-kN6krP`B_Eg;h zs8Mg5xXLI3!6aPk_Da;91Bx8$@4`wKFl-wwk;qd_<#^s%KX^zRsF2{hI~WAEz>bq5PG(MHqn>Z2{%EpqV@Fj2167H)B4nNxp_=l zjZE4DYFciO0)0;I<~pShDm9&5T+%W#GX*MPmVgKI!?!_A+yz&Ue|4!^`3*6xL4O(F zKV37&iLhnYk9>xeu(P%gikQ-n?L07KBPuD=7MRXUE>K)wj+YiUEu8q-=6pa4Y{u0Vn4%aYQ}t|DAk% z0xLyv3G4z!m);Fxck{feimMoT+#@o|xx_y+w3YvfO{k)q41a&4Bo0 z=C6cgjTP}-VdtvDia?k5@9C_|Y8+zmMafG9h?jLr`V}nP7MF7hXM-N)H1iR`eu5vo zmR`Hkpy!I-9vhIj3ydaeB!a@TQLce~3v=_bf&#gK z9&OTWr{+15*h(cm2Su^*c)mp@^l^B7RIM>(B%wCH?l(QLprD{*<@0<4btv9h*PXK< zJ8hKnlJPjj!Cjf&a?XR~FEoWi=b-VBMuoIY?h=c+I=_Z9pg7E`$WP@P=)PicG2pA1 zxU=8!6ZLUspJv}u(UW(!wn`@o8T>gL9yL*PHqK~BMEWDM)bH0`ngo<<-T}- z*1&AMI(m3?lu%Pr*!1=5*rJk>8dH=rOMxe<{^$r69TQWFa`ic@QO17yL>3(#U6cM( z-;jdVy5X&h+wl6+r)0!W?cOC4^Aho*3szLNO7_Xh%5kx?|9bxXIhOASIWql;6U&R} zsJQz2xZH7CP0k-+$ZGZ%X)0>Ud#;7|T)fb3^VQWd+hvo5TD#=N%O}o%TCkgR6z5NH zPVBxMNKwdyf@$doPh9}u&9X+LXrVl_R)8Y1h#%re{q$)V9+*bj#)frXwi!*$dbq zWAuoUy56Frqt7A`@L%2^LqqB3Q%Gc2vIMM$s!%SRw=y!o8GS=TTuSZf$jHd)E*}39H{IxkwqLw`;ybC5F|kC*~#cgPC=)Zd*fCMw0>`pXb%lyVK+2yo0!y7!>|v zmzT#U=xj^#i1hYJQ@9X3kaQw3E2*>o>({T(;4EO@tucl&>{@L}p3%=?l6MuQ-$I?` zc#3!yGl&vd?x9QP0HR2u^gTh6d$JFy0WcjGozlkX4eMnZd=~pmQ zLBR``2mhIsQ#2R~pJW-)m{F9Oo{n@GZg2^{L_USb0l>Xz_+yk4Z;gzGu8S@e6ci|M z0~l;DIKI&M#oy?ith>4a6;RZK;S7QBdo3+34Z@{9;hD6A1Tz$RWlvjI*P2?`Ii&zo zU50L%E=HoZwr+cajtEir;%>}VZ)f)%NB{EJm_~)7jGVJc4h_2qCW`CnrODtP!*5z= z|8zPu06P=Im_@?Cz+kEd6#{SxodQ9{;awnT4g(TtowI$86T{kGY1;0La_sTe!^B~L zJ#q>Ptj(TSsHCKGRH3CAzkmP`V)pZ=Gvzb}uH4pGDsZ+rh|Z^)lRfbxYE?Hdpwm1L zd!0y!zyN?D*fC;wcr;7r#hJViRiK`(o`F&46TdTDm!ht#4j7xVT(DahsMGYJD}A*T z^CPVr;EKKmtqU6gY|6!iju8rW9J2@DhQ}Xn1Hd<(DE-0TY=ihlx6KJ+l;@i|4)Bt^ zH}0i=??u5mrT>hmT8{WH_f|&9+=dN?q>$v04)A{2@(-)%{ka;j=4Svnw!IC-D|@Y( z{X^2k#01T9$oBf?1}$CV#pXzMhy_eQoGpQc#`JLs=&wz-O;?sGZrAOy_*3S~=j7x9 zTe=p^%B`RCVqz(C@HisX`ucj=1|$~xEIk-b7{hUotTCHWgOWiF2iA%*ct|KBrdwLX z2Zx7yM1cu1Bqn>gw^H?3C6Nwz^m8cWbJB|Fx z6GsIS?G4S<0P@FBx(!!I78is9i0B9bbBT#j*IGPw5NlbZH5aX0QAB6(4KGbjTA(6p zXI;F}(1UHT@Q`M&Zm)l6S>i6?VOSEds%DeZ5kQ~-1Cqku)5aS{_(9cPQW74iD3pv; zd>$89QN#CmkwvryGh?K#ZfRLJhHKF?4Jw1ogetRl zRKl-g?6gK*l1fBS32Qn|xYZ6C(=J557~BY`4*ix16|guxgWXTqWTU|ZAj?Ch82?eEj)Bh53)CWlMu z^fjL4DJjYr>?pQeuRG>sF?`LwOVHlS;v~SVYtbKkSKcQuLJhEN)2G;k|LH3>@FR!0 z0~N_(S?J;b3JMq!2Nn|(Gu2^eV6cK-h$lZ5nIL!no*OTvKbonmt$pF9 zEoPRZOOL{qzH_$sNjC4>Jo#V@IMeC#XXY-CIUzQIiF7uaLTr&g()4sQm!to3F$wVT zjVHc9O4m0uD75PX_po&tbv#jCum{2}U{>T$`MQl?r5OuBXapyFySunnd*)~I6_$n` z{DXso8@+Ss49VMjT3U6GlqPU5g z+_)M(MJZg3`^Nzj=zHJ?W2sk))eAq zUvow_PHt}D5bcT2+^?+_(xXEGv%|~jBO*r!kg#z_0SGrgQTJAwLV@9{rmTL8yg!+vu%6_qz2Rs^V!GB-aqs6Xy{2= z%H5wmq9&jii5lfTUlUoGtOF#01ekbGi`qK!ZTn0rECLnx*bg}w89<_1As#obFEDr1 z_ose2J&~4PTdmk&*O-$q1PA462OO;yiub*n$_X>3prX<$&HPNUZ=8P*kQk@ioDNK- zGQ$BSwWdT>Pxn*S$det^O!J~&P$U3Lp(?8|9oMFGpr-Agq#qYC8B}>OtExQ_&-rT_I3dQ5^-$4YUJ|M@Q;mXavMOl91abT4{vOI=;Z{Y_@&3cU&ic_&l^bx&q5|sg7r|b8QKgE| zw)X}Qh>mIIO3(aabBqvD(!Bdq^_IPJ=%*6`h-DUcr?xqNp=z4dUOQFZvp{q43@#@H z2tU)Cl?liab(KnDGcwHi9i1IA=2W+_>V}6c!B>TvH=YO3K<1aq{k@do}HW8 zZanCbJPll+K)!ol+1`FVZ3{*o+4+yC*>`zqR`wIJVfi7h(s2>QU8%P1##0|pz(4PL z$6QAk%}1lqe`8-6%X0_hXZs@Y`Um@6gdlee@)v6IJvP>R1bo3~ReSLGMSD2INj$LUj_`9so*mju(#wcHQ$ zIo4#={ToK%m~*}_&IQxao4_zw6a2$@=_YTQeA_JcJ;<2dU^Gu9Bv*I%@f~jc}D>j=AdP74L7Ks*9s1<|VBFx^JRxLI{{4)N)HdiSSpTefE8}-#_{m1kF-WTQ(^!Mw^VF{3`Nzzol6uY8p^} zRv2I)K$Hnrcs{0|1N!`nXIu(EE~c~H=rB_W9-2?C`nsny(Rx$FMA6l6_?EaIS&N$x zRQ+HDXhd}V*%|x{7kU4Vq{`kHF^&N-M!2G4@!vA(c;b05;$o6_<#BE95{XSQ3KM)@ zRm%?L=rxw1QT&=C-6TI%6cmS*jD)!{YGK8E>|*`9VMg;ZvGK#3v&ujbW&%p&&!asz z{TN;d01Ve`U&_jIj@eR)J#L+le;}cOGnGZPNjb*&$3*IdDu@AP83zFb^xyB_mYvrX zaHL)`s;fqnlBjgpUUzHQ+R)_l-^0k}zcT;`w0OEYBEM~HY{Vq`@I~eLQNe7x-j#|~ z2eE%iY43gRDjCDIX)kFx6wEiY1KyKEncW?0gXH?zHip4$4e3RZ?NrmkX|--5qXpfD zTsBPBt@GdE{kOw6({}m zg)ij!G9=h0ER!9tR=AhoyJBEEa0opkDS8u5>a?KNRrH`R|8hrN%JQo-y4fpZ;LEWq z#B+)FxxQ>>%yqA`YVJEdBln8f?76{Ua5|J9wad`8bu=cmm%Xf}!$;IhH|%FI)(t+Q zoYLbbT7H#pLb3Xkd+-PSGY+XCp6hv>7PkCZA-%+eSn z)(8F!QbHcJz&j?cXSx#rim`?SIXEH1dT@y?E08i>E#XzL(Drz{9e6GDeZf_czUFg%~5QIX4(2@NA=qF%6x>s z&zyg|GqOxYPQG#U>_x|FObVNMLN6x?k_0fJ5q1*Zg7x4Q9zW`94Cw~H)L5>(zRR(n zHu`;=o!We;#`YBmzuRbDAM<$~*d?}WG{6|NPT5UAjLsg9kCHub57GT-T_p4G3f2y+ z$(l3yEI9D#_t6U9eg4y`MJ-3KGZ9NZZ@mS=f%jtL&*+9GMfIn(f*xJkidP=9$ zWl7<|;x8D(1&Aj$ z4z*(3FR5UHNK_y@9gx7z9>K7L13g+n1ckhS-rZT%T%Me7Tu1Ar)p}WJUHiC~y$bt1 zWC4btv8sxdCR0+J!is~kd8ttsfG;k|N64{S3#;-$Jy!c_RN!*gg540D{-Z3?VYPoY zB8Z$-Kxo?GLC2gCK|^dd?c6A`WcDS;Th34R&@X)i&5Wz4jk-s|9wN@WT(0fCSiU0_{-v98CseShno?NI_;07Y_ zS%6#kA{b0J(NO3^jZZBt8g$vlXpqm*2PcR2sH+qJ`&8i;54){>+gi`|hQ0;vqSw8Z z(=H$L&@b5MZ~4t$p@ubsl`&TR)Je6mKt5a^+$!Uja-&|6hm~}hNVuP%*2~1N_UE@i zeoOOa9uTyCGT=8LEQEv^I@*^eMYWWvk3OoEeHV!Ef`41yz%Y8#HW`oO)7za94SW!o z;PsuIj)mit7pWmP$=i)}(gWl0-D1)Lj`aQ+iaH*QXrLXXDD>SE!1ht~b7okgg7LzC z6nyN+@Y2(X?xDLlIOwbRh6lQmvT(pgSCm)zu9Whz+nb5&HycC!o5Gja!_avumsYJp zrDt`4f>A?C7FZp)4o}|>)z)ekG1e6o3!}>hqj4p`K+KNQE-a&#G-`vCX2sdj0o867 zA~uz+Gn==PyT%Suql?rS`_yzQl&~vR;}|qZ!OhGqFiscdQVk5hz~9oxLnQJSsxkq< zYLN}+1@M@^>C4Y^Z-%KQd4!P2JKV!wu`DUx1SXa20zmi;))tfw$*YFOn+2}gyzJ=L zAcXoN#zAqkFwQS`qMPalQ@0P0C37*OLJh3&BV!~)`elk9fk+V{9`pzES!YJtab#%6 zQ$mSkhP|nxH18!{; zaFA)}#hVHrAb}>H+3Io7V~LpS-?gzz6@2JHo6V1f`%SQYu8H5I9E zQu|RVvh&&;YJ~TdSdGLQ?Ah6Hl*b5(bZohr;J_M`Eek8OjhLId#K45?+!as_k->;= z1`bbwkR3IuvXqHoFf8=#0tW)s_^mVzZj&+zNdId7FvxMl@0FFmf4-PEDsu2LBO@dY z0t1YE(u4?C7%od(x04`%M|^aM;1q)7_6>P z!Gc*@kiIo{%uAsi{)3(Eig~6yhMRotNnh8Ah5cz&GEicU{rJG6U8H@Gy?LhyrirgPPfH$RUN}23`;~cbsi2g%H19C85shwE+)mA#g?Ql^ zcs~OX$8TTr_7F@UQGAEK!0Tx4r6ZM3K*3SnGJW#;^=o!hT&TR($xUw4(SU_MWh-rI zwmj*UUFZqqTH#&VSDuqO1lYlC34GQext(P}nh>tT$t`dxEA zs+6+Jagt9cO1MH+tZ7wX`2Lp0%dG$R#Z8RAhFH%{UdN zlW)fIqrTksr|N1FyV~{S`VCK7Ew!=%?eix~wg2M8~sqyNgC4H0=SZ|asie^66X(~E})9{%l8l+&&weMxJFL*b)9I;Q{CVwl|%xXyoNWyYM{9{w0uGz`h=+l1xFvNGAR}EK7tYY4|>` zeFh%)bDNLP-Irt99iR%Nzt4mz#0kPXF!9F1o52>HcJkjtk(6ZZdr|R)A_;$m&%0nG z0R?r^R%Gix?$CgcQ1{YAkvcrf9~XcRo0G32)IQkcVQ+G#WUBg#MSB{-UWLi!T?b>% zyVWxN@@_8}9C7^Zxc}p6nwOa8-kGPWQ2p+7P`QTD;Zrj1sXf+c&+fW>>O9*@O#e~S zOV8z{I|;a4e>M!I-F`_m3~eP{_e>A|PUk+K^=67!#O-Y2ydC^_LiE}GJh8yb>L^fUa=d=)Dx zk@c@aPG~%u{lBT)e+X1`m&^af6c11o41jX^xDz5?GArM134C%o$jm|e302;9$UOYh znh6>)Bo~x9qX{r=RiLG<^L4k|8kN^bFcMWh#9H}PunWicAbRz1W_J%#S3Z+i1l5Uw zzsDTN5zi!L-x9G>qd_JoFWuTOzBrPO12oPv7*7Ko8V(Z`Ytn*ic+T6@?~?#koQ0fq z>J|FcXb2taA2;ygCT-dP7N@j(C}lh|_SVl*H@lb>{x~(SQOQ#yucgg)Wa?=mTrT%h z(7ogCbosc-H^ttell8&5l=&C&G~#$eQ|>kB#-DzBTo&`7105K(4V7+v zg5^smkG1&2D6+sn_K%fsDxKmvSI2sgSzBV>+#O;yXTeqNqZ}1KpUfgD2KM$Ld5xic zyVm2C%l;~tM{SB6Tl$SVH8HM0%15?Awg#21BSwRPms&}yksC1PH>;~+nZn6`b=wm= z$WcONCXxy#1-G}=jldP*wRfLfril4CXJBK=$;sQS<2gh^D-jl)G;M@Hfc|gl&%nZm;MuJK}TJY3+*$(#z>cybCt2>1-IQqq zrZS5t=fzEF1X?z|m9*pf+zo+7Zol#TU!^I1cpMgdCbaw8qebaku2gfHm^9& zx!P|cGkAGZw%~0^uL~6DHdz6RYxi7Sz{VRQ&mpDW=Js`5fAA-slV=*e0NzC$pI4O+ zPgcI%d^n;L8C69lp19y5=BnUf}C6dMVR*tf>&)ZM`cj%PMN8V-hckm01U_9ho7(^3}o zySYse)Tm>MKRK5i)`9Y$oGQlC4mWvNcJHS}h}S><)yE8Nb1W5D^^j`kdx=*#M-=Hk z$e1|r!ooxCjrNu>qpR%vC!MwI&b3cmhG@^ASeT>t3W9}|c{lqD4Pv7wD!3ypGz_hW z`3#+RQ(F7jfX_qy@Q>!Rqtim0sksI{43^IZ%y4bKUs4$xptiW*{{nT5{?sO~ODP-) z=FlwWjGvM~Cm|s@>2WOu`6SwCGWHItiW4j{Xr-1C7dWdcWtIJ~1@DtjYt_ss?Kn69 znD@<{j003Sp6g_Ltu?1{8NO^3@c3k-!8hENP)LHG;5ACZ0HS#EEiD)7p~AvwYIiEn zvIay(JSi@jB2z3Bj1dIx-u1)6T6P{!g71u`ldOGDWjV6j*X2s;n z<4;wx!6jq?I)%%>Uw>Le^HivcNlY2k`Xu>WiqUyHx|ih5n=ksln#3y7!2F8>ig1I^ zW8|EDjk;2&8{sxK83TvJx@X3s_j4t3jax-OosmSp1a;MOCAqlZ6*N1SsxLkF=ZpO3 zwHAtuLCm0MwL!h2M5{hKi%_fxTetid^) z(fWd^Kg|l)3SBVe7pnzrfo)CRVUEjW?h6LJHm8LbR#B^zycf(!3E_ZYYZWyPGrb_R zgSEbUd7dPxb*5k3JeTBkre+F~b&ZmV8BMJ+H=sUi7wL=AEK!ed2L>qJK)e4}v1Jio zJ3|)JnZalq3fz3kgRYgLSdrc{1uFvesK5fL{>mmiHrH0ymB0s(s<`6ZIyD=BUcm%T zV%9Tz%t^13JEWD?=CB-j)|)6Om>jpUn4OC^NLFi^g>!5W8SUv z2Nq!vg+ne-iDK&UY)_1v#YUfGS+Q6#-=}OZ=p?mYC=`%WLR!^6AUBH^xJC`+z~4 z@AIDp`Tma3Oo+XgdIaxr~4Ale3c^K=ZGrN z?$3^Xj~d}BVQyME6(45_ce_j+y-+Ib^{#>uzW)hUv z+N_eu7?lTT5h3i=}<8>lX1VjfB<*K#1RE=L_Sfo2tch=Gt@myk#j|UW; zQ^O6SNBS$VgU*WEXJ!tLy@6+YR{l4`47by%LPu6{LR)`M%K}C)$SbF-oM-B&yEDRt z@2~nFCt|7nlV2b6dE@@Nd>0oQYDxS}q@0{3pu}Bmbyz`8sz@wA($Ao|@BI3<>2K`6LCZ}VAYDAYThy|q;4!l`#H!76NAnvOB4PPHuD8g=o(uH zX&J99DLHyg?sOEKy|J&($Dfe)Q)scv^#aqkeDlRdIZqSSOLV@U8~Oaw<$6EzCLg>; zzqcm1mb2zEWL3uMfWE~!i<}wL_lcz9pT*P(1FY`=lf6%o9d022|HMZ?__P9CMK8reIYO*_8*xkKc1YTaz-4<~+eidGxm!|X)BGNWR{HL>-_;((hBYi4b z+)L}HGLnyLvNqgqp3d|=>7*wH}V}93LAH;d)h>SCdr^Q*4^gV z;Lo>9Z`&G-mhaLE+fxtG;xHh%P%_wJ%*}>yPJr6Gs`xQYMDPJN?28{7#P;8v{cizI zft_>4#4xvdZUQm!*-86>hN_p$n=B-L3MfJH-!mKcGnSX|YFagLXyk#+@~&B%V%;kIX&`RsY%OA^sC~gfW^A^G+ZSz6--%FX~hh-r6#yctR|24gJ^<$1nd9Uo=TzW9gO$m2icRfLldMn z878@V??qI;@(AWg*d%{m6vxcr4z7u^+*RF(&|2`N3P1qo^jB}Y8V6T|?nhZLiD4L9s#wBM#`O($zidAcojHKWQ2H)|~+-1dl7}=jS;bf*O9h zf}~VlUU_#e#=md7zJwz1B0eX?KPE1gAj6WmWo@4RDD5d~c3J*rNY}92;xV#)8PL?? z2U%))wOQbcx#Q5w(u=2$1;{}B4gtiy60^+ovdHf)mO7Cb4R?J_o$599&;y%kFVc-C zV&n*Yw^CdR!BooAaBxt>csrlg77F`@*1K2|*Dfb9w^GoYYx~Wm`Prq1J1x%oK$Zu9 z*X()UlRo9mL|BSV)rVU6-`YvWtXoL|JE$VUeB(U#@u5gF`PNF)%LzlO?zIYi_juS> zOe6)|0uTp;Vj}w!VVIcmdztzo;~BSx?DgZE6@m;|U3d}i#?JwAmKA*B<|#4#Ph0T? zS8Mg}BvOl`4tZlf?m%8c z4suQV;4Vmd>!ZLMPbD=}yKf-!aLZ#+!nG07S@~f0#);AFUsgYGK%_F4SwXqw zdTOkFmFPL>O=}hAG4vAgEsBy{A2BfR0~u=eRQ&qslFLn6K%Q5vpVteoJl`v6PR9Gr}W-; zwxD)xtE@D!Lms5+&7kpP#RBPGog@<_m$9E}$-Q_fZhm9(s!LW>H$xVT^lps*XY~~DRR!Ky{y(SJExp5B z{bbhBWlPnkWExHQXQcD8P5PX+c0_Ma8$l4yg&2<*vs!)%E-Gx6jn|4FZrBkrYK!M_ z{o@WKG=zV!MHy+nn~OtqB=%)}XhyOEK)1FF#4bZNuXdu@()dGLB7zn6KgrmMKwiOP z79JMEPInj`!RdU3#6KD?N7Lpkpsxnt zU+ zcYk4Pcl%`lChIE+UDbOl3FL`xfbi-4rA$ROD<12V;s4-SQ)0uZ1^8G)*L+(5<9UA8N^)wN-Ik5klCJ zi3ptwPUD@k%x1>Vhkar8&G{*6#0q@@VT?PxtHV!0delSuFI~ffyCg!5aujDw&a9$z z%X6O7QPIv4bog`=%I)6ls!3!s@uwuL>5TM_Y37X7!RI7W2?2P%dMNV+t7y&i1g)V8(JtGvv;rOAxAK*uk5S8^PiVt>Qk_;QddS^!c_ys5GG(K;~}B=Gbm#Tr9JDYP_FEfoCAf**rvcE@BqPB08 z0^1Y+l8`tE6nlSZ$rv;99Xb{BfQZs`gD6U$*x%>JqxZq_ z1?cy|=^v7dk@ea{`l$S0Y~k;$^GG2drsvs#3V9lMQL-y|L&`BC_+W0|F#H#o3yVDp zI~757Lt+)a#)sRHPM`^1SZxm(Hf93R-`e%Swt&^oM~nV8-#Zc(u99Mok>1MGDSa|2 zt^jr=1g{zSNLszCWbSn(-4Vhk@$skP(p8*UI^TTCI|Z|bg_ZD5@oKbSIvl%2i)QSf z9WqIczh-maHWZ_Ktond=l%q`fpcRT6*%MEl5id)?S&t_oa z#(Lv8_=M_DmQAlR&L~NQYY)gqXW*!X667?dXV&+Wd;ks7nY%J<7jDyqlaA<~#=R_! zKR)7|)tbA>tvom3PJ6t)&q?Lb)i|;m5B~H}jo;1MPI9lrU)wUnF4w(UAbUF$|J@t` zxCH0{KJz8hVVPrmtj7vd|LI4cS?%oRE1JDEsJnUWUC7<57Kn%`r`E+3rno-e6xK+t z9M*k9`0yM?3Ki@p-WFJvrO*^i1bco}@P1z_Dmssq-hWYM5t?r@rI_(MDt6l>-nP>9 zpxgfIg826vcAKUjsuweS<+7Rar6n6~YJW}QeBKZaNU||7Z!Wo3cBW=JQ*Jti=EqB} zXZbljyjdG1KK|_!^PVzw`Ul%CrISJKX?p?(-&~H>dr2KCw7W3<#Okd5OrA}dM~I&9 z36Xe(gXMQL#oz*z+$NMkWp&4fD$G?1Z);%7a;ZyW*5nTOd=gw`3zzfZ!H|U#k19y zj$E$s@0?R=hy(QWH;4!S_OTB8CH|Gqxi94?`DkS)`<#vdY|=NI>k`a@#|4D=@(9y+mZA8z{^xK zNjDFq_{uTbu zn}7HFpFQ9}{?o;R_69V?H>f|6LffLLMw)jzwC|}OBB)x*|5Je!`VCd{>Hm~|^A!5+ zKjVcugL?DdYLX;S@h`Sn-4rFt6+-a?BU`CawTI>Hk6J=+-~8`K9Rdi{g#^jubN042 z9eiV~w2-C$zy7o#dLXMV`pRtO%r4pMv+L`y*3*F3jB%@1bjbux#5J9)R!@{PqiOrt zdiokIF%!?oQ=ITS6lE!%Ol~FK)6cLK>VxPw-sw7{-|KUl3$=>exgYY}Cb(`y9u&sL zAFYg>`YoGYw%)cJxS1Z_6DfZ-#^VKgCJatQ@(qeHv9Q9tNVbm#>?tm*}3mZVvM z5jO`-Liq++-CHjHYkK^`VFr@;yBl!aN*8$G36cmE*@=3gjw%W$-uvYU-LvE1Qqp{y zY})dpu(%*O8oXZq1)?wfpidqoyDOpc25fE%XZs$pV{dhrr!58F8deSL+BCx#N{jpVQXUIay!MR>#*h|Z6 zY5RollLq)KFVg$1>G~;?x@)od$zL8I0g!+M34bTX;q^CvKsjx1OW%Zg_Y(;G<)w&j z?C!)=8osF81*G+@(ZueJc1KvT5akO}Z|^uCyTHJXsdqZx-~5Cwc~wpUgcC}jC%s)u3#W1ke1U$Z8={hnIXot2y^$? zn->$4EBB2#<$OnftZ^MYpJ!mcJ&GW@Z$-I5_MRxUmt( z5poQkF+b{@M@HFiX(alcrAr1FydjQDU5`&0l>*r!#Z-Zcc}G83kuWG^&yf=tM;s3N z020;PHYRF zq$)C+qe1>1OYo4x0aSL1o>kX3;iKP8?N)l+L?tBE$nVHqO9)DWWh7Iu`m%MRDqdDP ziN%PbqJ8367-^n&x(cTj+nzN=q4EKE$LEfz&wlvYRr=fBvSHyko=)YUYI*m1;pHB* z-d}a0+%y~;xkb7H;C(nmjMJ_lAj}m4`4H5Q^m6qc4(&PN80QG8_S4q-15!pelSL^0 zr)jN2D`rR~<|k8Albs!T&VYC(XW^yH%&s0?8R-|9MM})F+-cGL88JfBU!h1lsNwbRctaZ-wsipqqY0&c;c5qcLGeP51;(UM0r*oc93LaEVee} zNgwG)&z90-9_WP^i!j4V^z9$Q;iVFvk$_ycgGrMaH;K*dDyzNAw@sUxgCYqwT&#w% z{x?6|I6u*oGBKHE&fKQ^h?@)!;_E{fpFjC~xjB1w#<}_JaP{;-#xMWqi%$!s?)^lx z=j{lNB=*t7#KfNYhpDyobpsUs@D3Gcj{pOw=G<~;SHmzqZC~>JP^_qw*ScF=%t}p5 z6CgZkX;%2cv4K(-u*;L*-f{{j^hfdx?mRi|6BgDtlJs-ov6@JbyEX)b-TcMJHb8&J)KYVGueVBjqXYla*Oyw!1XB2~&m|>j; zDpl((D#LhYM#kLs`P|~4AHvIl(Knj3&3=BC?LUA1Y_883{f%}_Z~c32+-tXdv&O_+ z@4i(#NT&}a;F+Sko}j?PeB*^m2)>8$@J-K>pN!<4nu>bE<~I|iDb4f%Gh-cYMNx&f zkJq=R4h~}35$t+(GyZqbfgJml=sbpqD9MG~)US|mSq65h)6>(pQm72nXu}?8oa_mzB?ms5%r%1d28PO<3*;G{$Z%*v zY=161Pawns;}9Uu0$F+y3JRV-*S)B{R|I66~a{O>#}7|9W&} z_Rwo4C3@rlmxwNn9Nv?N4$slgI;1it?7QV+b=<9ViukCL->X4#k;Q6eg34xofYQg5 zS5?7mc$AfMHFF*pe}788vGR;)i{9egnPb(~Upb|CmdszD z2VMQR_dK{OnQz$NcZz;laN+*}Qc+$`3}VJY!~7Kr!?~(lN?(wJ0^W{nE~2cGrt|ms zIPe-!TIa&r$)&67+OIovC@V?&7(#f6Di-16@4tweSwh1P(btq7a@)r+d{!tJ0l^HL zYtk3h`Z4;oflf~4zh}N5%!++wPivz}d=%aYf?vT@tO!@We9RyB1d zdwB8*13n-jGdyvD0l-;rWOr)fmK{|u!;Bt-+UCBgM&+YwSYNl=Tg-yfY|6X`5Pp5S zHkq63kX+4Slo$nTnHgK7dI`iohpF}yA8UthTCRZzVF(wfagjahEx3Ph4Ovw~ybpx9%2>1N@o$r6tS{S+&e!7g?+kSzV zouznaMnNn6-LCoFzToAl?K42Qb*CW^hcV*|&Ts{EXD5!J+``!W&R3?1ZQIm*}>WeMqtpH&(2!KQaS&YLwz-0-ZtZOvK2 zOuvIxo5R+N`OTa|ub>&!FEKA}%S@iRNzd=K6IhB&kXYCqU1y>(dt&dh|K@&^flaD} zloZ{ysRYMjQhHx+uYJ$$Vq2?jg!70jhb&0<=ILkswgA7Xsw$10&i#XQNckfX`M$!6 zG}T!=Oapqcx1`t@(K%jj&3c0a9{U12%Bvz^&qn`4oN5Dq!_xK8(xeq?feH}`2`8k} zf}Rnc)wmV)wpLxunYr(=)d69^0RU@Q!S3cK-|E&%Sm+lGwnfsb*Xb-`df!G+2%ji# z8c}y8VBkh?jaFIrdC^)^#9^%BRj0U!6n%J=2KZ|1T9b_pvZIArDlTbLaQbazU_g1} zNy2A2`wyZUPF)F6qW#!^(=&@Sp0+m?Ct^x|;iPcumG+Q^{%t0bd?$nxJ^F*|=3F=q zx67OGY*_JH;CW{Vn9#^8oBx=xCwwOqMp598kIoT@w_=j(CGS{&SoB+mtk6$?t-Z*^$R5z|*sy5DoYZZ6%MjfeF~F@#SQH zm>OOjH<~NOBM2DVxx2kQ1eGw0;IIJZ@qr)L2)uuJxEnE@Ck&SHXF`^LlMd5U+`T6`{wGS zVDztc^hP>4DqBUb%&g}LbF;BjWM4+$b`tZ-K-bJ1e-6r)V$L#w9n+A%8txGFD2j5D zzwTxK)6SIuxGoyo1ic??c9~*`m?gxt8A19tX|6et0dw?R)ia{FAh6w)_m>d$t zi~}mKJFyR_J16FNf6j<#$d-64e8Yv?`-*=Exj$3-T@cB}k?879lej{en6iXniqnq= z0+(eU&sIKPEcj7%R1Tn9Z;MgRQOU=%!?{sEHNEMgnHCo3O)_agLkbVu8dxXv$N&Oj z`u&gaybN$T6f{S!)_&kcX3+Bfv|Wo}=`GN^h=U9`4#+s^t@|D5<8iYAo?{db_UJ~u zp&^)MS0R@)x3U_~GjJIzZ;F)5fJRP+l{0E-X(>5Tf;cp6+vjDtq792kq(7wL`6@wf zH025J%eY)^iVmNS&Z!Wt37XU4jJ4smM;V7bnbHz1Dswq^2T%K`7zxF@z z>^v>F&3tYY$P0yo6WeUz3rK(bC-NVyw_a&s&HnEh28a(eUD~g$4GQ?-nNO@imG*N! zoUMEUlVupGz8WS!EV-Z^_3|jejg>Ei^2@aUodbpR^z*?It8T`tTiN4nyxN6=X}q0< zgMe=&euUg5jJ|fza6YGy8NIgS`%d{k20!C|g~lrM zcXjyZN&P)FIM`nKM?3Aflu9z))iC8P<)woWc$MD8ZBsw+2H)W!V+%U2)t!+X+$#4| zNcuwu8`wayqx7BX)c%B4cpjbq(aoQevPc)c6qs1=S-_)(t2;{mZ{Hf@5hqzc2Kcvy z_w|`yj(#-@OrPufGk(u%X!F!f32YpmR zIulaR8*^z3UR+Bz|E;Uv$*m9F6QnMIbG>y+EM-CN=~l%Luis1MbAk=(myX3Q=EL>4 zOt9loH{sFjHO1$Z$A0aM%?g8*sw;D}MNE7H@&1*wn$JH(*-J#TKX}7rAhLPg662sG) z3ib3ZeuVqIk7i%Lcv8Kk-n=)smyA+?p<6I8he22{Tpyl5(0u^>&n1iq6e&1L83tha z|Gvfw0sOz$`ya)z@c?KU_#gmUE?OKG0Nt7YeXR$>W=rt@`2ByDX`%*my)Nyx-`XX? zV^v5=MsBIX#OQ(O(iK279eXa1xZU>#EYdHWj^E5X=r>5gxdianG&BRq4K5>Iz_UEV z#Q6=FS>v+X|El5t_s>A3MD5#&d24Juv^cdQJ0V)iPr9XhS7`7ESd}Pbq^3JmaP9hT zV05&y@OInT^SX{eK}RNhD$!dZj|i=OnK)!K*h8(EwF~!))#io#I1aj^F$37zL#;;< zPoHatNEh7}FA#3rTn=NG1C&MPs3p$d95L&C{jVTBm`7Z@^8Xo3_%SZ>-%8KQ#!5O0 zV58jQDdrFH7iva7_Fjk8`};q2Q?>;o2%;pSP_duKSbw{;uL)eU?J4-x zH_aWuTtA&uXF8YqzHu9~@IIgJAAIo7hbWfmP;=<47~Z5Scn6?ZC2uK)hPz22gDsx! zQMub+TlV>3uSE8rym_wTs9-b4?8m8ahK2L0{>x$ zB@dE9f)W}q&1Wawn?=dWD+mW^-8@2x_8eP^h_cJHFPx>`#?%Kqv0lHi__=(kJCrv> zi{00sSW&drs(Sf>{Z$Ts^qGWmb`AQiagbc+emB`mo z2@^ZOtU1%aC-d8HNB_lNc*QDl#O6G)?xrm>doeI>mF+H2P6Cb^pA=){uL&RcRwCnQ z)r~yLC5im?>mnz(9c))pyLh_;5EEZ@v^tYJJI;*7%vh9-Y=VQ|D?!O4?Gu8>WJfTe z50P6Ke}sk+idsu+GFCH<+So~jUrZ}+?~|{t-@B(3R^4dfTU^hoEOd4h5AO!qCHG(0 zWru{46pX>6r$m*Ed*KI{RF%K~7=PDU}vZ~H&A;Qa`DDOQ-P-7+ie z=n4xy^R4z=$7WCLSvP0Vqmzs3rgES?W8ld9uUMAMLh|6~B0U&>Du`QQIrI8?$x%dK z6f+Pb^^doH-4}1b(|0arjBi||d74qhKgG9Q_}(qc7S_|%tQ-M0A=pjn)d+gTftzdCQxMH%TALn3VtWvyB-7x4a zmD}c_lM5T_|GKc@BtCIlC?NoJ)(%EY0x_*~aOJ^(Y1?cO!*kI|LQS+}VCftC6A=h4 zm>8}YSww~zk003=zm*id$71Fr8rc^EiivG1P{!*7T-#H@7?^q|D*8}EOc1}bp;ihq z@Y@Bl@PT&Wo*mDZyx&edfE|W$7-K>x0TZ(XnN! z{R$tx31|g6FPkKz^gomCalVfw9r?SFy9F8fI~>J&|7BPXB2YvlHQghUV_1HJPpF8R zbm-6=AjXv_+qQnW2-zWj^Y_=-x`l4_q=?(MUK~Uw%`FI0K?6L#UjhHJs!SDpMg;(n z|NG>y>^wYCX(RmFDtzpLGGjuz)lvWiX{t-XN97=Y*+tkBGdRFF@ZfZ=;+PQA`tOR- zeJ($DL{d%^l>dGAFsF|01Ar2786h(BnSZ9{g&}cR;6`h16!$-}1o$OFaR4v=cg9jK z7IIfWiUX1_H_=z?olXc@s@X#*#YrrYIDhEzl!aB~f;zq-f+Gh1TpfG));DzSRQem5e0ZxsDIwr`%;S2i z>ihNhu=$(Nxj%m_-h2aSrXa{Xs)B(C;U$^t{v~XX=>$9T?I^E62JQkt%8Zl`8>|S3 zkHTUImNzQFy1~bQ;zPQCkPd!zR9Vdw%r~#|4Px5KRFYOMv) zvB@$9)>w9Y)RzW(*Q|U+Qi~+HK{wavDEcjUp)Xei!v(*tE161K@Ra zLSK|;K{(lnhJJTB`OEoc_X_Q*+q|Ttm#6V52w^yyqU&Xwv7Mb=u~Dh+`R(p!>67Xt zqJ4gRNr|Vqhu};BG;v)+Lj$eBxwcFyoMmyQ#XF_(MgHL*pNngk;`VK2Y$)@G`o?k?}_Im%VPZ~_SbjsZTS=3Ye|?Tqr{3$ zuKJkMOrA`My%3yjlyiEy(Og@RG^ex6=5Ta%EA}bhTX3W}jC7*JH@N}p;QIDrx5Rno z>mPc^^vekL^sZqn$lfTMGQu-FVk@0Kg1r|Dvg!UvLGcj5KDz+3FLGgk;8V&z4&V$2 zT^FL2mm&f&k)`n@8bx)3 z)5ZJt;Xo5cq40wSl0uU~1+u@cb_;Hm(U{}m06UR*o^d}tvptc#`NTDus| zB5%$Jjp0&2@Llj$GoHT8T*rqa>ALl20Y1Oq)TXNyV0z<#aCYUxPb1N61?uRKTSgyxuB5vqmB0ns7%xq4f0g9S_VYXI=Hsf8_T2ATUMVB z1OlKcK*X6kJ{klIt6dp&v)>GWiqfDG#OH=_Ah;_YD8$;!Z!dm5wPf+O`o!nGJDweQ z@oV_u_F`w%e_i!#Qo)cP0BP}1-FYGvB0VxRI=Us-R3sgAS?0Rf5bGVP_>n^Ba)E&L zF0UfVNzO!FtXl9SzpB%#uui{rRuVmt)TnKIIRilW1L^{Ohr|p4&~FL9y3%KMcmE00 z3cld@VWBegd$pa4@Mul)*?8;m(swO#a`MYXr>x6-jL4dxBYqnD3p`MM{7L1T)Zb!- zOR&Rl6>VgO5X+~K_d{oIp9^C#ynk{w{QA|TQ6^B^xD^v`1a~4v=R>F2i@`tH*%nP& zx@Uol-c^$d{UXi_*PekM9!-=l+XPBSx-L z9SLAU?MYBhCwSntB`0Qi6s@D{D>TGzpg!cC<3s+)+rua_;R@WMS9VfDQlV1e(|9@W z29_yFnB}Tqb{DQ|9Uo|lZ)+Tmm$`C*F*e|$v@*jU1~v@JpXpe5AnvkLurcR|9cAyj zD=U{Y7j(|SJuM|yV)&h##gkf=?r{vf6bxXJ-vI&->1qY!)x_;`Qga?RSY*(m)LnvE{hZ*fT7c_t?Ab9i*NuK3+B=bV(G9R&dTO=(OO9C~}cv)%0e?%&`0(Y4Ot zP=%?lIt;f*-zGu?($O?~YC&jlFaH(Uf>IM}yREjNv_`SYpbn|<{QjI^-@Qr2yUYE) z$JFk>7yvlQ2{{a#*?%J@W&5%m{LPDt5O*KTPKr=4^y%V&gJG=+)Vwn^wM{*_&{J90 z#4%K_@iJrsBDH1bM)QQZ@#6lCGU?#1Yf-3)ZW%_#iw9R25oC$*fEyKtz8iOR7z4(Ln!Kz@!JEGYrTa(@Iz&}s^U0WV zq{d2!9vGB&=I7<{ioWi8MFpQJoybw+i+ditd>>Nh9e*jp*ll#T8h#2`FYTO=KIvdT zJ39-qMq!%$j9cintQ->OmBgYE+moT_#-Z1+UIS-RBR1~0yshGpkH7Bs4O^o;T_d*M zF~b%>kJ5Awlv#`L)mRJT?Y*%Ph$l5{`C>`Ke}v$>}-@nDHy;YrYrGEY;B1 zd5#OI(KKj%-%HjZ8Oh#!{+a)w1Yz4Xa{>ZaSdp^42&%WVPvQsoBqug?dm8e(+du6BCVg0Axh%p z74)^aK3e=*!#Oj)5RBdyq`9Czzt*Cqmj_Xl`{`iJF!CL%PRIy%=l$k z12=AI90onY`#O(?YAhz0mh^e>%NwE&`tf-I5cNn#>3pbiHBhQZ+-8s&0@)4M3{#7I zSoiUv#RQ#z78Q`bf#dxK4^Nxb;nr=OfmF{qCb$_|OP-gUoS7bhhB z+{CbT=k7ZyUc4*a@cRhtG`k6Vd;6WDR5?O@aW0*Y*vb0hj)%H`)qtJ_zA>9q4e#=>CPVe_SU^8GWthOj|TfRO&`pF{}WxBDqS}QmaRjieqk{voTJ^gADp(tX509XoG z5#o7rDDr>_bt5*mhcn%jrC*PO%PT52FGyiGiyESb+h%kwah}SZpLlfIIfllSZ!@g@ z%;DjlszJo)6t@(k_a1>=sW=PzD3%*h`(sXm6IvqJmv^Xl^#}vIpP6Hy%neWK)g?V^ zGFpI#hJjvp9#2Ydsv47zIcxcYW(NH!v#fWRe9^>SYSABf!WZjbPRz5;n@BAH zPO@0b4;($KyHC0wyGnZt+^HOM(A3`xZLe{OwHt*L+y`i3^Y5B4+m0T-JuaAhcU?6zwL%}8h^hnd}>x%)O1EHKWKckUkNag`x`P&{q z{0kvOWFP>ktg_w)fU?V@ob~~5Zh6#R0ixh-xMYOAr5k3vQvAmnp$bNbnuGaw7ZV`_ zh8V0$2+^uU^qWepFROaTTd=cP3G|7k9$d`&F2XC%ll3EWPPsQ?}FVr}0aUh?<|p4IlE53`y~$^t-@uL*No+Z*${%UoeE(gF9Ot<5)-n%OaGyVwb)gs z&F@>^7=9fOF$v-XzTXrnvdIZZ3u8x)RF#Y#A6`owE%04gs1CoA#ly&wA^{)pns@pE zg(;*|Eu>$e=>5O=dLI3SXx)MK^kianEqwVXB-ni;ulZ1-2ZS@DiCRk4CCM|dHH$qn zg?>AH-ktCMz)G|p@TslP7c;`~r4zv>(Qxw^;71B#m{a&)1Qs6kfBbM|60k-i)a0z1 z>^li&7uSGe%y@|iNH!deNisG3Gm8oKrI$RuM^deht0GRX~z$>xZe6VrY zU814=yk&|(sJ>;%F-CQ|SVmIj&0W?g*y2k!1lSXOJ)TwiIgFm=3-Q1S+ASwnMPKuC_K^y!=a#kV-13l;>;AzXpj zXs&9l5vf2-jZ;9xY{UZ4@dPX&*bEKP&rX7x{J^V=D%-^a+rnvDzMlf%5cGef55I@p zksSJw7Uzf#XRC{)rti?f9T61;kPmQ_f>jy4ID5(V)TLDc4-ok_#ztRCkXIs`!UU_Y zI%=d~2a7u*Y23!PDMmn34I1y&>r0=ogbTvKqbGfi`jW1&iVFXarfif#Psa9Q|JPi= z7co-HkzRQ+vZu$yS;LuM)ves+Y&I;8DyuFFwLjfyv5z)oin}do+HJD1KY4v&YFeZp z!o3>AvBy0oZZllYGu;{Nuc4!}b8qLEB;I10c(B|o82Qz-CM}Rc7^3W+1HVAAqp18SVyJ z#m{9yEyg)yukBU)HwP=jcd0o!S)e_+sX{p&9!xn4Xz!xhqO=ijAA> zWzw0oy0G1HR=?y7IQT(I`D@H_u>)p&uC}}5i;vP5hkv_Hv=I-7x6WoIjiMST#3soS zbgW^@2|x<~#U#*vnJm`&r1nf(nar|LXI1+Hv{%l}nm(#0Q2arZ427z<6u8$=GAUo{ z3xBbOI7CxS{&NQ~;;LxwXa%e=SzaQmH0w* z79vjbjsuR@Y>(xU#X!+WMJ}V>Xx#vpKV!Q$GGQb*M9)A*qM}VNH;N7R8X~qPc$^TT zTPSWuNZ04p9j7LcEG)#8S=!=)9e+2dY{8>vekS?*Txm!m*#Glq@@~JiN{3 z)doLx(;E6K>cut&UBv#b>uzmLX zPkLxEuejA+h)`&5vztcmo>3|Cpoh$pZZ^ZL%GU%7OoR#c#z5r^BQQw*h@o;)FrfWz zKB}u~ACNtoR2XpdBIgG&_%SQ8h!*ul1g26W2te-E;J_&l&!Z^gLA)UFJ})lBZ=DhY ztd9#ni?BE1GsXv3>D&H`^Q3!8(3U(HX)jI&ad-GHkv*GN>@fqK*IjHl@G*Q=$GOVY zC{;6iS`8OY3?@rimd}o4$AbR%gQ1Tw`?WGE;FYG`;oK#kVS!-}ec4y*CZ(MvEWB(0 zI1K~D?eeb#QBYE^m4U414+K9eeZ~nFsj_P-w|>p^KQibbBCuDeA%v#}qUQk!h8GWa{d6K856T<{qA{I| z*h5H_ehRH1Px2*q??)opCr1z%Hx?u`|VKpOc@xhgzg-}t0yL177KqD0>&w7f4 z57NyKyQy?X*a7l2z9NL#t~`hdJmUzZlY?TLRI|+P1c0y@ewZ;PC?T>hMC_fYNsFf# z|COhYRwy_$gBcky$>?h_KnX#$roK`%mYn`TLS{15FZtX3=0a;Q$w|~>v`u)en z1vMmuP$GMw6qsJj(6=OhYj#AXqsAE2wms=yp^o>u=DjMG$)x7LF{=i%ntou+{H!tZ zD1FSi(|ywa)UwC3vAK;IYO{b1(WUTQ z_|svL2P_b5j%gT{w@B=ZDny|U8t?Yt$>t~#M1Bm53u!!maY z%Zi<@%Z{&eWEiLZ6!LE=%YruoX!^jWWx*xy{HsV=&q~eNE+}%4Yxm7`bUD44EOg6= z-sJDShi30GMDZ= z2c|^x5{!99Xxs36l;ZC{NCfdglH;-`n{;*fLlaJN+a|?_-YAtENA^LUQNO*9@BHTM z#)iKIQ6YQ4wo1C34!SVJ+%I@fT{4>+t7P3(S@`AkeTF`^wM&_L=Gc+@_jgQlflc?N znaH!nBhWH%9~>6*&fSqgWYPN9?Sve6WnE1@yn!&rezC6C3p{tdFlR-#ff&J6bf9{c zLFe+Mh%NG0>T^wkEGFjKB5tq`_$63F3e?sqEXM>+sSjTO;$On$ORV+oC@G8QeO+R) z9I~13WL6)&K8EdG&CJKIPdBf2Z8Wm|O_u1?LM8HP8;G#DL-8Y%lK6y{x8&eN4;hmYN3hW06 z;?@K5<-5Ehw`94!;WYDm{iKnPgm}WniEqMiuuof6l6@>qA%6RQzXBy4`VgZ$j4QTT ztKnGE#%b$05@Tmfydm+LI2$b_j!FsVg`>Uz%!N1fk8%iW6@Un!2(u< zU>dQ4Bf z*>R2!|=uS_v@TB-0-cY_AeD?##Dt2(>R6r z)QE~YZXJv%PFB<}Me=hmZG7Flw0_UL(hHb+gn~i`;a|~Pily2L(SxZer<#>?(FWgi zIq57*i=tt@-MrwF_uVu5m!ZB{w>saXces3|j~qIs2gW=eY?gS|+iQv?BDdab5Q6ER zoFQx-_;=^bVsJq*jDW9L@$Qui!6s}VjCNd$ot7U5-{fF7hz;u;0$ex;Ux0NX2NQ(; z_TO3L)(sZa6m-|{jbWY*bZ_{lG{-Lq1m3_4`$Q=Y%Y(Tk`o<=I1%mxxKflY)8SG5D zQAx|tTkY7qSil1@Lmr4`{yT)AoM5OM*}^v4IMtF*n`Q1TO?^v|Q*$KXs|n9MG&o8` zQMH|arDcCf0xwkBz5Z_b|JLSz4p`7=EU5mfQ)-#JWv^uZ%0tLMo7-n~o7p!uD)ZJP zEAzs)<*lVO>boC{=b6Jp%eA*S9`AF}fJsNE8hidr(5>hI!L`KZO`z3V_KsdH@Mdd3 z9M+LN$+IIH1{2p2=};W1toj(7u>$UJ3MPB3dppRXBX8#R z(vsf!sSl8Ohq@1kcll)4g+FSbQ-1CYKMZR8`y;V?vFk^vfAal(uT(z~BU?X4ev&LB zw`&;!cdjmpaKx;)uw&-44Yg=MGBh9|??H!lkQ8?+-fBg;IVNS&l(0r^+#O-C3UOQF zaSUS@gXKBhB5Zddc8w;N;P@J^Mpd`;?O6ph z#0Eo^O6HeAZ-z_ zT`Cyh)pc%dZ3Vj$c8x|XwnaFYcn9*S$_ofv5{z#q@TM|zkj2hqHI?ZjQJ6_}HfTjr zqTw8HWvgH!r4~KOsv&{TG{bEtfX{S(irZsPM^&{j5?^MgwMS}5(L~;G0z=Rj$=Ih% zO@qo?L_p<`jK`3Dj)8ywGOfZNu1Z8wG9OMn=5|jIw#*9jYECXpS_nn*W;8HtFSTm#vW6NBU@{6SjdI^#QIR9 z4=SO|tHr;vr4meIoV`V;WWg6y$uBh(>;7sgT5bi&5+|JGsm+qAp`WO%vv)0Vw3+e_6%LZGFR;rA*J*=!2>uJ?npt68X$q%QHs zcPd`zN#}hL8o9yi&S(t28wuK5R7>&Me3cu-#=PpQgC{P}% zFrzqJAk9@XSu|{7^plymfG;-ny)!fsPXX4$pVjmFw3tylPzkvX<;2!FX_r?FVtdh& zkq9M(QHlSqsGEvhA4}VtaD1NBXsDz|B^&?k+Hs-oAnrz7O&W(>{G&Lm|MOOz8Ym!Y|oI8z;4cfC)#tvLYVDOw?Z@U09h^w~Y2k^cEh~}vEV5f8t(%aJ-n51A_7SSw9^~L42lC5bbmbou{lh9o} zh;u9L&sldmM%XGtWuMRG-kXds{O4nJ)l?KxHHUAGW}kR`;`Lde9tFf1pVC5gw}lC0 zEh{VpR0}W@^jTH-c^oY03{25O@j^w_yeU$N=L1Yw&fWc2qW3CHeHHj$1qu5{3jO_gC1_`mSHtH5H@?>M|ga5ya3(46Cr-w-io7D0_?1nSvV%P1} zlp7IO8Qs8pyI$dA>u<&E%#|uMuQJc~l!~SZQ``3o`R5!g?)pMR_xElRa5g<{5|Mx% z!x1D7_BK7{<`zw_$~M^p#x@Vi!+>%i`J6IId5qo0&JzmZjI^;-782m5{g<92Mj9UVuJMt}dE9QGjVrqF-nj;7B zH+|HiWN>39?)w7XS@DoxS{Bdo5-DE{h zIz>7kF47dB-ZK^kv(Q-_vrHh_2<~SgJu51=Jog)^qZNES&kewBQ_|<^32Wx7sgc4@ zbtEOqRhyqcC%%nS$NHP;Nl+4Y8bWnuOg0YI#DoM<`y7}RrK)fdF~xZDj3vL>4P^#V zy$5@y6)(M}*yOBJur=0+YZ(juXxt58vCcsAUqxAfM33JHgU$Zn10gieYkNAKM*Tt# zucQyj#}@)lZhjsR9=mBfFs+lB>gL`Az(*S^K0;(uNohy}QTso};;LFc#EQ${=ykgBN>}lujNU=)|s)i+RHl+U%KQD9=VDM}c{?m&wNDg`r=igCpG965Z zJ4icskGRLDn7l8(oZU4XHI`XB0CONf~kmW zLBqx}w`T*{AMpo*3!Y+zZvx;SNM6hICjwv!xT&SoS}H3e^Gm6GW_$|XtQXx_jH%pL zC`Y0lW!FTI5otYpTa2OIW%3ESncBSkdMG#fAbi0wh_$KztHMW&DRd_H9(G}*%9eGo zfAjKBsu)|^<(Tyt>?q(9PeVK*Hw6VkGK_2mWgNrc$3K0;Qf7BzbFWi6C6=$Hr@cvP z9p5#NzE^(xUeQ%1mtC9*uijRV=UVhvY`@yCyy?FveR6R5)A(D=av8Soxo{0C#wmNl zRlhuqfV|N^(sCLhmekf_N=ZUWyz)MLRQ7x92ae?MfWIa9T>)y*l*S=x1%7CG4gdOr(bFN(6;!OQln21uh#f zWsiJNZDP7ZT!spZ%5f<4;-`}uJm1Vc6GS&6v8DMS+BeTFASR^!szY#_)v5Ex0!Ap(cORAn94BSC70f5&RPkTwyW?w!*U$@8g#^@Fk^n?QDq3abIDt-%mAJ3QIEz`#TFva*1j;XEb9)%C`oL8oKYEEjcZ z0eZxl9t%|Mo-z1rrX8SNbegahm~)c`NJnd#XvdLEejcTvg>n|c;Ip9i|+G~q)&o}UJy|) z`t@(Ho(cP&k$rx-`QNbZl0Q*m8)yeaybBfLU{cy8r#$$S_!ebp$8OJ?P8JvE{%lOp z;5s)E*u84Dk8sDLyQ^@jItkp8Xq`OZ{yRy3?CcpLqa?LPX0HX9U0tY zie&*FfG-ZUPl53Zd<02D&S07|>ONI9$07slg7j42g*$o}=C~mr0~9_c!j%fy*Zgu$ z%Qls}O{JveE{_T^3dzXbl>5sA2mlNR#t-J1W%@=yV%hgOLus`!bLqm;VoBU$V&Y8{ z^-~DtsVf4|Gg2MZcz5YdQM5HuQawG53D{NaRicYUo>>|HWCqmric$ak8Nh_2x})M` z2{svwRZ*j){})ndk%DV)&vM7d2$m9%T3Yj=wDoBb)oMIT^M30ps~w^qVXTX(Hz0bp zl7sfQV0Re`y#WmyLvc)tYb?4RzMQ$|HUBs((%ntRd6`hD@jeX2FJ)g4G zdd6RF@xWB4Lsq5_fO1A-5TCtgBL@;~L8z7pb$m8fA`t`az>uq`s}nu}kb&V=G-;k4 zq75r9L@T-g?Cl$jCnBX_Jy1~3i5UdceFgEMPmOO{P9rzim6eO?3er=6QS6M2w-=h6 zG7tBl6cXTnKF@tR6q7(gegXiPu23NHRKe8Q1~z+a%F4>@%14ytlj?Z}bI-)jd(z=> zHY&z%efI5ky~>+?*y@z5aw?nL!jTzg*nk(;xjVAd^b{^)dX_hc(`DjSC*$79)B24G z?m|7-L)4zSt8^5yL_jViq6MEvjUW|Z)0a#hCnpeMqmsVC6wTR0?=}$kJWdCC-e0&; zpdC=Q>U7VFvB`$EKakG_8^GYm&B38`ABo2)^y)2{N*qaww4B(3Y4ne{&sP40?95E` z%!&Uq5eOUD$zVqSzX@*Uoc?~>sbb^wYFnATX~41Q6KV(wm6JI?BU`2l-uy>Q0s!#P zQ@ZFKB?SXj1~CgZ?4wa>d$9AV^mQ~X@adT?W`AtLlIU+~72=5ONz((hYbA@6YzCi7tn?TG=A6BUd!=8!U zQ;&&HB%xuEQ~D_jP+<4VN+kjkGt(_jl8TE%ia4T*4mUQol(9IN*#xvHH!y9mjyT-j zx|)?O3y4TD(62o!v=57R4QM1~kyGn%11Rj(co|HE*la6DFPKH{4aGy{i^v-QCo*>OKA)pxmP-EjrW+1RgfyT|Vi?>lQ zx}+thszkh6#Riz&CJZfWLe;ADTzy2PjJtPk^eqN~0EFg}X1^+%$aGM@n)Fn`-4A3u zZbB+)6gI$Aj9W~gBXY_*gQ>p$;6DYS3C-E6LjV?Hdf%65r@f28-AI2Xq@7&sh5;bo zNRvpoZDA1;XGaUiiN>+S13ZvN<6K%yq7?x%S#>)^JFvq5q=4$Zx2BlUlPoezLI8kR z#R(~WvI>;_4ghGRqUp!oP>cWo-ci%W;GQYQpwH-ORM2P1|D92(;EFyEA>{-BXvkG? zzYSdi04xHuY}fz=OCf9m000|DISl~7Cyaw&07%J*`-J}geN)EtHsK@$r9%P%0Q5&g MRad1($tLRm0^~k;n*aa+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36717327cf87efa46b6279448c8de47bb3efbb03 GIT binary patch literal 155251 zcma&MV{~Or(=Z%sV%ydvnb@|IiEZ09CbsQ~ZQHi(oY;6X_w{`LzgqjOv)AtGE_OAl z!sTVf;9#&}fPjGDB*cXkfq=k?fPg@kpdh}#5KL$zeg8q*iK{yT0l^Ra`vE7>z~cac z=#WVW3n;s-U2LxZS?5O-QD{z(`1}f0@;?wr1x~V)Sd8yLw>mO$fA=z5Mfe`$|3CQA zV8xS4d_SCG#niA7|GhJ*85kx0?`wQyA|@8uzxH^e!hs#i|Gkm+lR^x4dPx2c`4kt6 z+&#ix;lG}qrm+Y5X8-k_tALFU5eqE%Z=khb;@AXH(f^@YOk%ndu@T_mUTJI+IfJ8TH7vZS$o~d;D(95O z=Ml#yUWXX@+^u!DdZ+uJn1vdh`$k`c?qx&{w|oSX)vl;M+mCH@^Z%6F9yVv_Xukl! z(Q$3Jj{|maf{THK59^15{%)HghlweNOZx~k%JdGyZN?%zCo%N#{#euZ1N&|@LrDUz zW3o~a8*7J)+ZTcuQNYJPpZaFd7zPg zq!lJ{yXZ)HwqFHYP_^$kz9uG%Dx?yDAm}%$JPy$l?}Hj997!uVE8k;#%Tf5qCsLiS z--Ouk`|;pN7y6cC-Mc4&%0FZ+VeYd%zilzz=R@6uS^Ig)RTg1vB8*YQ7H_`^Iqstp zd|l0pm##Qo=zj(x#Og8W+TJf$8wF&H!rL!J0#m@OIwQJVOuk2#;w?GkdBA-%xB(k;q=P@n$^u?a}x#%$5AjiYV{^s-Z_}?mP~NO3y5mC zcnogWc+@xgQk%I$(Si4HXFYIP|A^PK+5&#U z4&S*Uo;0#M-1kLB#Gg!V}!}8l^>T z)@>VoW~0#l^E4nJ5Get0yH_0 zMaW;gVdjHly)Y<3#_joVhIx%4Is^mF)d}pA;w}IiVTI%g2erPHgsr5i+du8u66eo` zpzantU*zLa1xh3v{!fQ~%bL-ZpklKg%C~ifgmIL@7^6-iRTX%?VDHQH*W7u+kkGH=Jn|-8+$k$R?si}|1k*^ z>%yDknR6y(Cet6BTU|{*jUK?{PZyW#OH$VAcYh=Pxoal+!4e+Rx-{aLge3z2bu%mLB^4?iK+db_>PkT_!YhDP<( z?pF7}UAqyfsQ()e%6ElpeWOVq6ILlctKw;-x1Fz5-ZcO>--gzQ78Q)R7Py=>c~RsA z=qIVT@Z5?;Q5Qph&iFv+-dajKW7~Pe#TM2IdxIiz)6fvXC?(as3|4a>bqET&rBkyeP`tT-%50f<5l=OYw@1 z$)zdcZoGR98d4SDGI$wx`1`wkCXc?HfZ`<$ciM5d0#$;&d0PAr$J<2{qxU^PDr{w~ z3Q>9pQCdT{9{*HM(1P+sKQI##;V=F(eglvIpb($g4AR~YS0O+LbEZyq^py0#|N%lmdGU+X;V_%TbWyl;0T`GZ541WVYz(u6Vp zzQ@Gwvq$eQ)W@9ZqTd=9FJa9-zCk$hSzM`;MOeS-7EfktX4Gh+IC%bX=DPGCBDX3{ z#FKFeJPtMvt4WVgW8M-bCXA``ttu=wN2#5x1ZNb2y3uFMnKn)030FUUmyr0@kty4~ zHy>jhr?o{K8`a%}vIcPlU7%o)1+01IP)bYVLFA$tiW`LXGKLc6F;dt}9fh*zdW^^b z{I2jIE(G!1jjf>yQk;&ZCg0lMdt6dtO;`a>I&B)cp{e-(>niIHScUEOd(DoHcM##a zdIE!{<;BL*`;R=It`oUW&ggx1*SU<{ZMq+V4VwFXw^O%gP-z0N%^g`lzu*+6Q zgh-&j`mxXUng%lzA*;J}cYk^wV;~Qs1>a0-=Zl9c!=g$W(m=(LE*fN<2nh(r|5ma} zXEwOo$$=!W2vaOn&?GsNQk;7&3`SMJS-ujr41j`DzMIJh&ckt%`E5qiR5ZiQoXZSX zJlp7#GOn*yENwDdGxE`^Z(@42k=5$xdOX2)oZVU+b*ELOn`!u|4GR(2i}+KGVB!?g z!E!J2)Qmpu_El6rM8O~^hlZM(k?o1Zps1`2si2_XJ^B*aNVvn`0|(~}^DuS)+DpC# zdDq&&0L0qb`kr9nm_77E=oNZhqq)9=ea2Xu`KH=zJ)#f|kx?fiT&^8J%wHGd-ieZkjgK&}SC0V`=!V8zlgx~so_58PDr#9JvVkZ+M9o&ud)ZMyfdfp6z1z%=rW~TgMMc?W3vvsx<9vyCPt)ZBLygblAv;@GA0TVU^FC~T>6=s&b zn{4?`EatO!FoG^M3yvh+^U0(&+E`g#>|#(1`@)G91K8aTyA2Nkg!E=$^@llf=9wK* zJ`C5xicVw$vI2TIckezN0uut7-dCJ!&6gV${Oj2D>w1#gz7e>CoQ*8o@iw=1_}{h| zy8|;K-qLar4^d7grbQXlj;Uj1T>a!@8LTWRm#xoFpXZMc2SX9l1i8UeVG45c00v^F zc)u^UeI@K~mG7>?dB=GZ3aPP*TnF@N2U+(%FuU*?=u5im1D zn4FwU>OjxJg2us7JvyT&m2ECtWbHI16|THEOn5ob)~fTvhS-U>l&i z_fV<1om)@<%A12QIy#!HFNYLf(>Oj_SaR#vQhx4OQ9&!^JF_0aSG+FYWQcR(qOaMC zob*$>EwZ73d85M}w6oKj-M0(<^z>(hTGjeeTh829mCgqU0>LNU$4fJxkC@#x=Mo$7 zv+HHJBuZkdErHI)8?uW%K_`B?MsswSmkCETS#zPyFAm?=jX5{3x-AQjZRt;mfAgy) z7(wI|bnWP?i9RDI$8SR8G%YJDFgY2$p}Cm^7k<1k z_D*ZanYJHbH>Bg$5bhHcL9`aFk^aNmW>`d!e4r^RRgZ*uEsL8`A*1r}u%1VY@n-Ys z*~1QzEhH4N17Em@&##)5ZU>>X>`Q}>_v3)c#+NIEMwPdJn(qVTVP9QUHltklm4yXB zf}7pj(*qP0UCo{oSyS=%@4#w3K!9t69C_|LGQi#h2(vQ-ZPC`lPSZY+Y_|t3AWe2>&w^H-oESN zDX%VpEOI1Po5;lt+hb6h+t~?6LQ=ra`zif{NuzkyS`$U~$B!QsfsTJ$e&*!lL=5Ut zEiCa0Tb`q^7*H_KVOfMn< z>PvRc*C2jEzp^qaYmt;uBL+UcpQm_uc#=!w8-o}r+v21Fy}idPtE&QX(@I2|S+W%I zqlJ;StQr2}hsdk0E-rl*S;F)w1+XmPFF$Fq6w6StvGEIPYZcbCb-%)lSe!$>^O6d; zQjIe=dFia-g9B1iF;%SraXSv3#kd?Q7lD!)$~T?jB_fY2+uOq{E2$&;9Ny&(C0h60)DpA4886$j}W94Y|d|qG9vJ#pDZ%i{^7N9^!a< zcrNWx4+@{8+P}BhMa6XW^z`_n&aq5PO^~7nR|rnUisI{sFVR8XB}X@_oeXC?p>n2& z?c;XTHR62^?%W@9j7qVWO&5q}qZ%83UtD^ldP70{G^?Co{*W$Xp}hNrIZyx$3IU7R z0>}P!&5mg;@;p35bBs|OH?iN@Or=X;?&|W0=IB_>9?CfKaC%y{6?u@Jr)4)Eecpd` zWWtUsn>^~_q)i=d{<=RV zP_$buIxAT2_qUJiO1D@+cO;L~#~*T)pj6g(ILiq030Z&RclYmb#JOHuyOoOTw~v<3 z-~Qz}%ZPh5^88e&IhSUQ8jQb$Fq&K&G$^<5cE+(VCYGk*lgLbcS zCB(K(QAQIiun!RpF;YhfHSEX-4-s4R$E>{~AsaHV7MyxxO;nk1CFkf=aa{rmpcSI!^x z{7T;BPi!kPluVcXZZ(WewY7O;YoUs&s#2UdAI)1sqPDixf?YANAWcn8l7&qab{U+; zMn(+isB2rZwf8LY^L}h;m!NnXHQ%Y*w^p}_w;v^7_-EU z#^d&fn;8Lq{BWuxD6=Fq4vnUeh~%Nk3vG8Wan2{H)z=BB2?x;hxd zPz2zU{LNB?OXF#I$GkmCW&fs9O82ESNl%8j48j%+D7* zj~C7Xsr2tv>5jnIuCat2IBUg(2!c>tUS2-uSY4QRV5_RA05bOf+W?tkUkS3WoLkU$ zFF7T)YxZVoX}LFTv+=B4{eeg*t4UMy(HlIeq%%Y(DL3Z!@d`=lUq;iF{hCd5-)T9{ zpooc^{zU;PM)-@9I^Cg_j*dm9ibZQ5*RuV4yG=;%y!@CNo1TvS8tq~Jwg^PKxjuyD zI6ptXlwDg}8x++p@5__p*zuF&+tlCo4q!SW5ljXSIItLCcvSbZw6QtL%-t^cmpi>{ z+@Yu)IXV(~gdRU?{Rw1d#t?^PgQPr3cUD&$gU5&7^Ii;lPZQ6@@pS7;>sGXX8XD_Q zgY#U(MA2#5lQIkDBjgg+?fXF%y}iwsNWtY<=YEv=j+R8;9-gM?aQOGxrg2o0%=oBu zXecMCCd>{xDr_N2D+=2Jt(W@A@rB|*zRC4THl1>_QzVW6fsql0yPq~QoPi2%{B?!Q zjE`J*=;fhn_J2ZfbHNS04{9CxVZjLC@(NlJlN6Bt`lY0A&}Kx_p`NNs$PG~gMf)a) z&k0J`!>W?FD#js^`qu^$h=o6K^9L7~-0}ek^jj23DOTTH*v!YJM@Yi!n=bQWNUzmL$A9p%pl%WL?si+}^ zl(KSSSX@SitbD$_;5a2@0s4&-L0UGn#e5mM3gcFW?NSCzm;jYz=FcUz$R7K;L56^X znY&gkc#O7*EUqACr!Kn|*DFr|&oTE?mt*#mXVM*$Xx_(K$VvgW zO^ld9ySh`H@M(cyJ&7hXzq5G`X7BY_D<|;0>@Mi&YP*|2pd@EaUUwp>bBbChVfu|9P+JKtlqG^Qpn0Q*)7RvjmB$IPd%;l40Gg8FMgHb@W^$|i8l_mkW?*uHxZrfU4A!%jV+yu-ZG z3lc4)kSSL_;kM7ep;;@x*E^0E%1?(w_p>8DJZ`$=Yp=FCGXz=shILVvWLC><^Qzuu zG6HLS{Nc(}H+4T396Tx3!r$+z+^WvxDM0EZmlGD!d0wak8?+leegk`ryzQm>gpK)^ zm1S6+Zk_#HZ{`+vc)R+=`zguD*~X)s4;c7lXQ}-x&*;3}W5N0d5dzpKGChBYa|sjg z(qbsJVfY}_%_?>wU4q~Dh*4?b4HZ{cp!Iz3j_3r0yA*JFz0`uH3>>rXUJ9oQnuOgT z$~ZH*rRP%*pX#7L4OcI zb0nIFtYU~mA+j9gjb!wU7Oq?eA1HHC#3lrM2R|dD_hbK+1d5ycQ(LNqNb?c+x3Dhx z3~iX<>l@}?bVKdR`^c!K2k&E|{!;wI9~C1xw~i5RgrpHI|X1rb$-yq%Uwh4&31ldS?xQD`0$;{@Pg(msr@tyY+4hv zCW;p(R^+wd!%-n6Lo0eC#$TOi{p0N-&1y#<5Up{)Qrpb_A=re}7M|HCLJ5;1+Au1{ zA&2B~#r^2qX}L<-DAWF|d`r=%cU``}P?JZQd0hY^fd`m3@fOJ(T{cs|DNh|Izyb_M zT5|JU3fa90AT{~6VB&aAIU?^7>|W;#{k_bo?{s{?QaJ7!rMJ(~knk;06yQQ0mD=?u zHKyRjBX*o#!6&*pqlG^7=_cqp@JO*R^bl(Z44ALg!A0@0UI(X9;TASNZM96Wu7l>P z`KZ#KhIxv)q<>+dN?y$QO8efO7#Y!G4=JFO4xvOei;EaW=E-y0?fBiyVkVL=aSFDf zbs_Rqgp=4u`Vx{w+&IndZsmJQKi_&jN4a6|Ziz+XB5CKxWQmIxMb#^=CC16Dd*%jM z?Lwyrv%7T8UP_Kc-`sSDKsHO4U9#D|Ur4?ycIDSlS=-EZ93K_UJ)+kUR>z0yUH#vYSDeHn5Yl;+W0vQzb65x?H znfD|Q@nc9-u*rQh6U?^|wMKc-;o;%(`aG0{&Ux?@b#Z@xAPIMdH3-r! z-H{cCcV=7W-tz%-pT>H?QZiqFqIVwE60z= zW6SXhzr4`K2DS)iTdz0C=|M8h3ajF{JBKB2X8hZJ_h~#Oxl$h5h&S?#=c*Dvh zojoY5RTp)4N%P3DcQt&4nowi2sZnS$e&AuxjkIQ}C0ybyFbeD;1 zzZx~!nwQC7MbF*(DVtq5R}sIp9o3~HT>Ll2W92O)OwMO#Qf=U5T3+%@Q*~rmlEs3E z6p#=O=+%=yyr#8QnV6bF?=iWRnM#rz$wXix`3A$AQo%Ho5`APPo1+4;?S>pE@f}mA zAZ99bpA6Tz3HAv%k4<^&Ta>7Ia>}?239Qi=HNs>TJ4KlLL>msG^`uyB z@xHH{vz1Gl4`x7fLY!ovMcpS^85V`l#6}vO$FJr;X*kOfzxcdVJ35{Q_fkgd4$^asYFFVB@v(< zT$_^LLy_;r!Q{##(DZ&u6%2W784YDYWu5Ts*q593wiw z3;Z!Ku%CF!YoPVno{N+7K;0n2ce`VIZLjs9HSSdu5loqF3CV)r|d=!ogEKr0* z4TmqcUBEI#Z3Ma%W~vOQHbfdkbZ|K}Rif~$wTPJ!mz^=XA*e!=mV$UVvP47~#eI}b zk*lWsE$1`AHURxDN-k3M>luMyLzlWuS7e;VT3(~8E*rh+x5qlV`z7_dmI24sFrzqJ zzuIVRd9~6ye?Gg6k#>-Ibj<^p!M*ydDX5qUiP1ho>3HTjNz98SDn55Yhx7z@qDMoq zQX1MlbO0e#f$WY&71?1{=>F?xqw!S3O@}}FU zfQ&4-xVru@e^bi&XzA|O6~EKyzw z*Y~qQ@OZ^gVvh+0YBuEG?trGUk(|Tdo-f$Y$0fa2M3wkNN)rVQMMj+)gY{;)x&6iQ z&V!EhSJN`I=4N+y*ZZR~%uUzVA$yY#h`rsX{@|E?n6pVM=SaPs^lPn~Am;Vu)A`dK zr=|57^aK_>CN8;U31^`J;gTjaSP5`-)xiS<)MegoZ&{DSIFr4(=EX@d0`7O&fY7>7 zuC2P53K<+{WMo{EI$>L79e<1dSKH@LbwOPWmTh; zY7BK#W055lQ#aEEdgT|)#bHPr(ZT|`47uaJw||L$zq_(uf30A_Li=CV7lUHIu((Be z;9u_o8#w2>eg9RXu*bBW*;6}I8bm_I^N|Hb_PUGlbdr+YPgcH}5QL(@8Ydj7h6 zYNsOB!FY5P@{3pk*-8%fUkJUFlhK*<*4DHUr+EkjUpN+gm8C~VCd8PX$9q}G(vWy6px<~aUDo9pY!>LKvOJ zK~NK2l75%%^~74C9MVox-&PmVttl8gEh7pJ@l&ef4GWF!r&>w>E3)WO`Qv55ZiG9% zc-C>H+GuF%xF|GQeM#`w;$Jlo&=eJ2I#;P<<=&bE*JHQHYn#%Pm}@pqi_e71@W0wA zJz>^pDW9V;{2l4XbkP{wzL_J zw&LY^%vbAitRp7{vZGu3tl#}qcDWh72ABiwaIeiRogK6}x2pg2k>YZxV2|Fbgyw%ar*i*T1`e`AS9Ga%bTAkeAs|-v3PdPw9w0h))Qgj4rOb8Jz zTrNecai;@pahMbqO%u{v$76MoME`X3?RGIH%8y&!;p6owA(~brYMuU)VDaa1yKe+E&IIkf;uv9s0LC#`oo*J!_HYrB*+NfHzXlrXzEV&b_ zRFOg(I_y?QQ&v2$WRlD zBV|d~z9DJS6g-{$87wMjN!|SHbQ4RJqlOXX3Ai6P5*c~-z2wzWRmQBToj1VDsS`tn%pqg6lJJrYIiH=&qe7S5G99_h%;sjwiA#H_O1VR} zf+SruK@(BY`!1p3`EeMVeAHh?bL7-h6_cR0Mh*pdU?J86LU6SKjWTxI~qL z*;p52(G2dgKsa}}H}6LVRo1HNF-x4f?XLix>QKTHDUvIK<^zrxXDc&2B3PEkN6B)N zeWoB<@%qkieqkc#DV zv&OBR`+HD$czAp754d4z4MGL&4pT5*5^%4+w4-R;yrulYHP7oV4;D$yNNBAzZ7%epv zP(QKFtjr#rZgiOq+c?bD5GxTHAvOy|1p*T&nazHgHJRLC6dJa%OBBe`pU%v%F-OUh zK*6cOwV{u>UiTC$~?&5WOJ}aC^(hDs8NTV+=kD0Y;ws-t> zD8_=7vNr7(xPNwp9Ej*;(Z!7ZY0_QCL=|W~MTkEv(asBfmPcDze${=jTVRD(&cq^y z_4aAC{4xu%v|8;@mSCvnl)YB`x-vx=I1gdk9evD>lGs78!&T^xpnYlB#bpG)#mbzL zw|qXhwuYjm<(1@J+wm^1$e5Ys9WcD_Z>)|9AA0iiHnS0$Fe;}%R$0w=_#Uj#inlSe ztDohpKfT%{qte7c8xyll5+_cG2*u!9nE4azP=SImW75v)9)9;)3i=ui20SyzcW7*k zWc8=VQE5*5CVky1Eo+W#UtQm~X=^Tj%99`W_G*F!3Yf`C@z}b!>>&$xbbSH~e&kFa z$I5n_eCPt3GT~ypd7IleeIRRk^X6#D03JLVG+$BYPlYf34C~rw$8XoopR(VXOQua9 z-z5ZJrFzc;M02&U+G}rM)1*T#)VnA;qGgXh`g{!n5B^jrQ$qy{ zYVq9|*RKXa7(HBUZu<&(6PIYwEAcu?D^gs}KO`LMUE?iUpv8%Uecy`AnkjoRVMk5$ z8h&RX|AU>|$C?PuZu&i=t9>U|9*P4{j2}nwXfIz2if|gSy$fc{K?+IZA0~hc?fE}_ z44=h;LPFvpu|!5T$zCl^5-=JGoTx&Lf}rp$mBqO;i2+fAL?YPO>7pVE(&$Qv)Gf?` zz5*047t{~jNac$m9%eJs)6fh>>zCq8t>b{L-&dfqaid=H{vdgNn?3}7U>^d0 z-&PRKl`Q(}2TKrjJ{KRH+cawiv`|<1eYUS#t+=?n%!kj8z<>ffxWA{_jet>?DvCEa z$hiZ+hX&^Iv%EieXtT2F5LqVHkYh(ll*|vZ{!Uc*XCSTgGG1O!^by-jVvpjTv54+Zb zL97}yt#0D^R*8#`?&eZFydU>n+9N@p5I<@Z%Q$-Mhe!dmfQ|t`q?eW5>hUy%k&Nh=xZBlGofdB z^Chg^)XpKW0groVRHH|L-vL<<%JoRzQ~X-EdjZ)mTkoVsK|ujBcXKZ^$!0RCsN^AA znLmH1J98QD3y=s236aOhPO+Om_UyH%tiHil+CJx*XqeY?0tq4}i}y$dt?@xy6m5Au0yKhP~m5N6;Wp){3pa1xnP z#F*hUER+ZlvYaocGA&YMr219baN+OyxXQJ@LOV_qty}q9u?(zCR!6I`!_2Y)T=GZ2 zRH89KalF+981f7R2w6c5+zO_) zpGa)m(!(C)mxX2hi(bS*SYVM`>VNQhbA;ZC~1!b z8;C<7ihop;jQQahdZskRLp1ISiJ`--;r+>MRlItqMI%=Q9UV=S9ybt?e$eS~gWjRkG4iUsDMl?fDUa_^m>FH;YR0?NnyW5Sy5hf%~-{Sgr4(z&iB8LxQ z5oG{)jBY_g6wHghQmqyw6o&;dm^WWus5_KeEJw*7izKQm^*a%|1OdQ0tLOdk(_~)% z{rM&mat|gxj5}18a&cjyMZ4uV+mn#2R-j`&R$P{PQM&Y0fdguHa0nbs)A4S+nExFD z2fYFzjGYxncuR1jbu-Q8K*P|G1V&QnjAx%zLvDvEM} zVqjewX!xJom?+GPGAhYlA? z`Y@3Z$+Zx1)__M%#DUoUd;c%sLQub!LvKRZt=HGo^;LmDh`}S-?3ePAcksl7zTDu|0X77pjtFa{i8{=tJk0te~&A-dsi+FE8Q$`8b^ zlm2Q7V?jiMFKKTCEn*Z2Gud8i|xRqV>(-UBLtd zr|RQ#Zy+(V@JQvPLb_fc;Bxb~SfsXmB5eK2a#y{J36SIUOLXfP&S5|Xv> z-heY7!ZvzG)U!b-=HZOut$pN}uR^iqf3(@|^2Tp_FV!Bu`^^j?Lh+XuihhXE>4ZUZ zxxY1=MNZRX_%Rj~<&YvrsEttv=6VKS#N(u3C=vWV*o9C6O%`_6f^=(hDP=t#rE3Yl z2^<71KgG_xDi9Hm*DG~hz~9{jzqXx{ItG^`k|zYWkZw(ne$s?T)E5o^(ifD$V8E`q z*>4heXwdDHWK46F{WbW~Q0e&oREYy*@%Ikm)C7(9uFq%jip^9TM1Jgy#p}=)?2-Bb z{h3@hV6%y}5(VvwG5>b_UZho6q3bs5Vb^==(I@GqnepNMCy|-Rg`~{rSK5dW)_*R9 z2xWwN_K-b)_LA#9lPU=+at98dQJ;;wgWz zL%BDS(XX`+BiaaOgUQg7Ily&`Dp?vPFSkqU@m!EKJ`Nb5N+Pj55F(3afN!CK{yHrj zx;>;J1dP~=PBA;0eHBvIvtQzy@OVC2P2;c{ih_dWaJ+sA_L7-yiU(uAMmS!1gRnma zUEf&2qoyJfv2PP|7GA~e%i3@q$-ziLNhIwdOuvWLa@a>VAS<8EvQ z9BPa3n03cU8XFtON+1GOFjz^fHhm7<@z2jJMBrat19H7;5dH7fjqx!x2?D5Ag>e0es<H(;HunU3;!Q19z$2Y5l8b9(Hn`se{!lL7zmK^!mW|_mRbpysX_7rY z+4hpmN3}{^nF$q?FcbrxDleiTSg7hx3;Rxx4Yg`^&f#;w-X0DSs2u~e=7CP871VF# z#J21vySk6G$p{RTN9h$Jc=qMk>PS|{!rV~VSKvfbz)YSH`VvnJZUW-ptLv+c7kE-4 zFbFVm?}L#t$IlXKv=a(hA6FavlY}W^9eEr&t(LH^7EZAzI4nhMFe5%lfVzGj3b}e% zZ^xg$4l$3Mdu?4{=dHEIb9pRm=v82Ih}K~bVaCWlOfsTi12xlwK%ZBT{vFhS&PK6ZH=Vjx+rO*pWXfGG)I+4Dih; zE?}j09xXH=O!SqsF6%P_DtRyIk$D*#)O*eZF7_7pWJ?OzG2EcBsA!P7xjD_kCot5_ zcB3WEt1J23!45Nz|NmB+9ppIw`msKL?U8G3gqg+U#8nAUYS-e^);GSb|#W}gJ?u2sTDxHG)I9P`jjOH^+^ z${@lKB=BPe7VIYrt)PM;*P}U39*xU z1v!N>k1s#Nb02%<5N16dxjk1NptO{gKzk<4Y?8MO?Qc^cS2WPYbPb>>)Vw4xTWnf%>^V*^T!vTbP{kR4G#uO`P+};$rZb zLLn|`&Yy|gt;DDRJ)06x!u5 zpQv~CT=T2$(tt=S3=65|ti!`YVslSYnOXqqb>57$dRjt})yVz*m&if!r;YnSUEg%= z3lJLjmk_E0-1GWP8fu(65cEWwp7g_m67-XcXS>;Kj69MMb|@0e|2M7c@(6#}K2X_P zV!S%CyY!-UbrD8r+>V6?c&OFJIn@)wQS}JYi)4CIfYckTH(8g$OMKeGUy5f+jqu@m z`+jO3J?|z;0t+dmL`Q#Y?4KnDqPvvh>fQfP3=m`q%FO0;9+L=J*7eOoHQ;N#qUp~{R+~FKW2g< zzxU-IyQUY&RZ1m4&kJDfNcch+VEHTX>JL}js4V^4N_QCB(%Kqtw^j7ISx60;cU+_7 zDRKVRx3 z`?@D_Tprsqr*^QRV56`B@)R3_^8fafh$DXG)8@v<>%8d(?$dGO?8kFDzC>8VHS`Z zS5)2HfTqwuxytHks-QL^0}fB$q7~p$6MXk)r1IhqyjuazaCXzFOl3ufgfNJ{VJxw1 zA*t>(xlCI@LVJ(X{wnSE3fpP*=uwXM$hmL>9F?-ilH!04j?U}8AE%=fIiy-M3tGN0 zh!@}6N;I^jUGLqj38&nxecmwA?D=8B6e2K!Kim%dg53II4d}mP!$DXc?}7^V82g-g z31s~Q2qoa?n4%aX#8F29j*x_g&~Q3kUahMBTXn`i(2k>abfNTzI=LfQy|sD2-k&0C z&h<8HYXyL*r5evuIpxj~HP0!!0(z%Tt+Pa`JRDwX_>#Y+z4F8OBZZiXm z@C4}#-Ou`HYU1F~Y+cnEk5|+zDs3(QefwL31~dI+&u5dAPZhiP?oe-yOvh5^z(WL3 z-apo`LNu(Wo}1@+I^1aGGkW*gEU&K0)o8jUh(|%fId1xlV%xfwNjf?je7qaFuUX}! zxri@OF%klYTCpCIh{OIHQNSPSWfZ;2ukUlp1>;mzSz4O={{Y%RCBJRu9p+$M+JurC z9Ws`_L5!kWx3KPCS)VpoI*hrgb#p5@Fv%~fPPCG8`xRwEq&!@=A1BW zzJKt+2f_da0)apv5C{YUS#fdIBj@a#*foNp{wizj19c>h91a`KH+c_5#AUd@n-D#z z7$HWaddw9PxP<~7e23bP&rmPngi8&jd!^E(s0^wkBOe#?ox)dfB5}T@1cKm1aj4v8 z5QEAIM{xz(kVDPqc_D{9kML2*5AM@gKx5-Gke{f!RA|PW`?W~~!IC9Qq%_jKkT5)t zcIcz4ZtCmnIcQUTMHW7 zs!Fr^D8i8H@J?Kf8#h*VM}5~>>s&;bh<(PX4GhhnKI3=DE+gJvMKuWFL{S__MhGZu zPF|H}DsB%P_)=QEdJUu(pBo8Rk><^vrx0vx5dEO?w_Ud?jT|$E^{RDhw(`bQJ$7vB z*uNhYAqAySNvd+WAYdoxE99UEGd66TteZJ=riWnzNIb|*=992>#sy)yXU`tnka?SD zkYm$w_0@0YBp>LLpK}!a|`=kL+CezBw_53uh>wY zt2tF}U=e&eqqsDi*yQTT*49=wP)Olnn>Ej}S;KrO2PbW_iR6T5hY`N_THapr>*}4Q zJ>}#?HIJQMxw3_4WBXlMI@30mP`)db21Ky>BM9`FIa5x_va-(;R&R>eUt3uB2h6pr zS{E#MkyIFAgaUy;AP@)y0)eZddd!|iFi6M@gkkUhXuVfQUP&Sn;tfY`)v8rlNJtR~ zA`_)@f-saqM#W-qQu!H2RrlmMzzXHNvACG$h@4c&GZkTZhF`Dzk$S#Cp_O`;LIHV( z&UsZ@MQR8d4`GI?^r|-UiJ$z!Ir$_am1mRsl^chL-*TM@#+=jabCwGK%ih`KL=l8x zxEaum19$>s<^;3Avq&NvSx?;H1#pK;?;(4}lgI{-Ac+}*B#u0jeqw5V7M;fQz`RdV zsjB|1s_8D$FJFCMEj-ralqv2BOrNosJ30ASfy;OM{TEmpW69{dKRb3f9L|J2gHAa| z9UQ#D*COXTA0htHz9Ik$7MvFsA0#fn}IS_0`Gz zY+pT|apxSct@^yhPcH5?6n|+usVMW11QJo52J(m$KN82KP%Ni#x$KH9J%oZQ@K z>7ziy`FxXSDS@hT|_HCqv0NcDPKm*=G5Z~Dr0IL7S?j*5tk z-?AV*^p9s`-ZIiLalYkmMg+(ZLI@#*5L)Y4?RBoT^#R#hU2SegYFVt8Ng>oSd0E~z zueVv>p>FOz>gJZ~^lYyFe3tjN`&U;#zW8yq&M53509;;v$9E%3?^rJ9hvh=>0xWJu zqY=yfR#{ec)sOgW3~fExc**FUTM{g+h>^6qWUnE2du)mGaO3-J&_2qAU}wD*;fFup z;~uls7nUxA!GItISQ{0AD@4xEzp}qr=Dh|)1PLMU-ln+^==%4I02x9EA%qY@2Xv`n zdHyPaFwwCe@P~Y=f+WlA49Y?dMxmX9<$s8eh?RN8Lxi0WBFu&mLI{7^yPKW1fhYi@ z_fEL9k|q$5pL3*75wJl;uLcCI@Q z(;8&c`f6Rb==_wBY003}Y@}p;AcE|qwA!44xCw5wE)5ZtW?x#bq7&bf@ zO`BbKY6AcO0DQhYz>eE3dWMik@`o9lq}x#e0000upghp071Qn5hmn0g%4Yxx0002s z7H(@ozRg;X00000u#=p!G0llie@iLx^z<}5?pkI_x{$7}z6|Gz-mocDYxO<(_Xz+1 zz=zMgtOSbfI-k$|I|Kj#;1IW1=U5m*p1yop zjr;rixV`-mcXxMt^z*dZ{QV{E`22Zzc!u9F`~d&}0EUnN0001h+p#}MX+sComO0D-02iZp z@eKeNLh>)A?f;^(tb0WZcVYsUpsd?aBn2(HT8&x8 zP>w~mp{y~~x`?PTXl?bqShjfnSeJPtE}d?J5^Xv!yrWAwR=pOz+Sf(tYf<~ApGner zv_ALMzTac@*}2Z^K1q^Xu(T#5Z>Y6KS!Beo)gZC_-M=VTkL6`uUtFyQl}`~d%_eiG zM2-i2HhdLP9b(|zWp%a#&R^>rFECX%UjqObLPq>y@9bjZsICC~oqK0ycb(uUqJcoE zP$fc8g;1p!)IxtmV?b?50&k%xA_62Hs+JcXN(vNc#T&dJRFww$-l{J|spw;As)qJ~ z5>b+>6(J!wC?Ur9Z`U*T-cx6kwYAue9WWs5^GTm(c5KhtUF{stoICgIh2floarncI zAG3ZKH1DI5g5Y!7S;knNG1AN!ZuK$J$Y}TFWa~V%#%Sw@XsbZ;ge#`aBjPKXA@R(E zzo3-_=j`OvFpQN&qIcZ^;OHVaW)N$TYZ%5#D?~b==*@}FrXU%HVHk#CSP^n5RPd!U zfiIuVkw#AC3p!cGWZq!7S>RKXDe0r_8ccQ+UFVT3Q0h394^SksX2KN}zM;6ufC3`I za-ymW?sNI$SVv34FwT#n2m-kI`hZv=(lCrwN3ufXibYj9hn*)IhG7_n6(P?@THq?! zQKR6Z;!;Fnn@l41@yN8v3X!YsSWs0vRt&>fQN$Q=;_Uj~urH?h)KF@i z7ks8@r=B6lgl03F)R29vbrY-Sl0;gk08fR@0X=ao3wFkvOyNZ zijbccNdONtAwO$N@UDZ?aSGodi=z~AoW3g1NP+%-MY1t&7`Tdic5P>kFKNmolE#T) zl%klzWgQ?a`9i)Z3PMVhp~5*gV-gS%_Uzfiv(G-eq$IF!-#&Kk+)1A2ON!kU;MA#8 zTzl=cgb?tVpHuV{k?x6Ck3&O4Y~H-tK1_zO%1A&2Mu8#VJ%tm%G2ke0OsIVa(=^SM z%Ca;~(_B$jRn=WTcXN^rtr<2Qdznap2NdrES>eb% z;36TE#1QE76`^u8qo+{E#p^HPYlGJ@aP!r?ar9L_9Qhw^O~ChOT+|X`ktoEGT|g1? z#EBF9X76wL+kpdhA+W!{pUu}?!{d)X&gGYGq%KINgov;R-8(0jIMup$TL_`nd*2=T zs2{)l^2yDoS>@W2CPS!TT?4PzA%6_UUR@bC9ZUVnRtH;X2yx zR1;rBL~I<-SiPv)1;>1Oz=;45T%zTa&6lj_>sMaJ^2PD z>DC=;yN@*~F-64L7w;NYg#5%f00Ob|AyboqPE6#Dg8tT8T%J?nkT^;k=+APtZvO$7 z-FP`~jr@y!e}0i{&1q8Jrtmpy*RCPDKp`HOI&W8?h~g36IQ%BN@41I<+qUuCbI)Y9e?_{)dIZm!V4Tcc#!9xe;yGbs`Bp8yF`>EAk0NkVhD&hy!SYI2`M2? zNHHNIGtbpAO9B)HKoFSu`RwZhNOmi*aRHs96a`U0KvfrP=^!HXE*UBExtJU%;KwR?czCM@%XA~&c4qVUqmi=@IeKl11FJ_C)>O|atxnuVqjxN z%l@^lxE_!qGH0MYrPTeI^%6D=<5S5&BC>2b>;jl~Hl%p@Xpz$Lonh0lKRya6f-7;- zA#apK9b;nrB*VjRF+O$_pPgpyx)0cW|9AN6b$v`szQO+ezb5E$T76@z>6@gPkC8ej z$z%jC<3NR~E_}BlLINIp^f7*T`|a%6^K&+Bx|)p{N0N)MLkW*!w>(A!P{=-hHrj@ zAKi5qM~@vtlhy(~Nkv3@{3w}ob-^L?onj&%PoWTH1h4+w=r=20K4zKmm^0G^OFaE%D@fR*RSEFeJ=sv z5js;Ho_^+OZu<7OYM*%W3Cd8}N7*nwLn077{PRD@Iay|mH`X&j6KSj^;fOI1Vk8D7 zMbIR*s6)i}2(Cv}7Bn%tp7|ViR>^8PyY6yCvU$bT%L~xY5!d@zrxj|^+3kz}?c!Q| zi~l*tU;G$SH)s809e>W{(`H@gl%zVhtUlaygT_9uGCW;IPgcB{sbE?nxK+M z@)2U73?2Gf>+o9QNQgR+xk;nT@bDQ99eSOoo_Y%L4u{}eF>`02AOIw(tr;C1Jv;rm z?i0CV$Bw!mYb?%Lhvc9&)xie=0SV|^YT3*JsZKBaA0|Nt0PtPznGs|noAu*94($j-4B=ht0 zeB9Z=aU9H0h*&Jf?A#pjcpQOF05vp@0|Ai9!j~WWc*be9XN(=V)c9HIq>ISA_#S}# zw^Jk#RZrvfMaix#b-yU%LIxH6NiNG@zWcg*en9hU{{O}=gs5o!0hO)4@_beC7yFSy zQa~w&!1s?DgOq<&fb?y0gvlBf`B0co-*(w|o2j!v1`fwXT;+Y4yhQzc4b+sn>uubliMccJD zh_v(l>pMQYeZj%c&)5P2BS6&HnBf{QO>!Q%!7mKI^mJ?~B}&Rj!0z520F8~8Fc9k8 z!|`!asg#mT508uxyBp;&Ha9nEYHU&kNPzka^$ZRUl1wI55i*y{(c02VPj@#GXt~@> ze}6yD$_kMi-2hx|?@(L~L4G$xwL2WohS7#TF zv!V)`TU&3q*w8>Ea#Pt|*E@N3{yFcrx9PjxhyRIGF*qu!b- zoGKyPC>&@LTPi5V*zfu6&P?r}GSafJPks(Hbu(Y-t9`pWGrt{p{=M1n{APa6F)}*J zKyM!a!^6YU|KS&h0oc8JH?#NdF*9?IwF3i-QIjHQc6awkd;9n8@+{yzm*=KRuD*BiD**1@9fe=h%5u^>p29fPi*5~VnI7hWuCc?gbzEbeCeCtfPO=B zDx+rZ@`Ss~T&k!OF&`Pytd5iDMN3ylf_4y-;haGcmpB>6+BIuXN{JX*?d$~L=+UE+ zjq1qZ!_@2bLLDUXasB7lOPM&1iQ;H6%26UDO=hHA|3E*!d}gieE&2EN-UHy@cWz6B z;l#u^&1TB%+^pP}kZS9euve1GQ`)yq1WYz=*oe~J`9&CO<@}?`NhWUG5HYctO2Qwh zJ2Nwb2Ap%!2MYo%eHg%_R;x*wMx%~pK$7i8sf3xBfPfX6ofZGnphBbR zH|Mvw#vHY*K_Ad(pv!_%`F+~JSjv!}1SuON$-T_^?* zP~YO=K4m1Dax4eIez3nX_RABo2<#F>NTpQi-cq{diY)#tQmx)X^1jIqEd;TG)j{xh^z(O zL}3wU-~a_hD3t8o_~oab;?l*796b0F9MId-i`JT2XQxCpTJFdC_3OEM^=kg*S+Z+y z*sy^pjzo-`nw%nvV?ZFH-RBnS85}=8c#ut-HgV?cSt)zr!UZm0xcs>(;ULnXQ~Xdq$Rn>s(@< zzg$H0uq@5KA4@2!{QV_Fe~7)Zazdw7Q5Y>GrqcJftG)e#J)5)JCUlfg*ASZ&?JQ1d z3=ZojF`NPGK!*sCRs36*XDb2H3y(!nQm*@JNfL6V2LuGP6e07zLzojI#gBM_67cus zTK@A&AWhSJ*6fglBp@WFX%|*mP>!eqaR*Sb=vtGwYo5;TyZqzbv;67U_j&)%C#kuA zQHwL~-JPXYN$Bsc(iNEnrk~Kzp_^1yhC+eXltBZv&YCPMK6&aCzdrFBKAM^o=QDkG znme~|lf*Fv$WrCW$w?6+(=_Gg&6}J)eVUPx5xi5Fnwk`0v6-dZx^;_p-nl6IK}IN9 zKJxTeo+ggs#YjX+CIeuB%oy+J+RdI>HjtMnsTYTb86O|x(&fwIKrJvnHpbk1gZ3cy zWO@19Cro}A?gx3^M&3?KngR>gOKF$JU_aRZpR#X^Ax~?UNVCY?=J`uw z*>mLX75Zg_m`}>AB$U&D29Q1*3*J8(lZ79RRSf^#^}h)P6a`4XY}?n7=*cuq7mtB~ zfr0JN`w|~&bn=lt(wanfT2aQ`#95C#+wM98+`oCOW@)r5Ly!dz7`+Y!;|O31nt z-Tg6xYZ7|9AjYBSBZ;4&IbTE5OQL#6=o3~{ACc`}U@uSs6evoeP`vQ`F3z1h$Mx&i z85wz%ZQH)ao;`b*tJm4GWeY_N3=Axaj#6i6Xov#`4zPd!env+}F_}0h_AQ)WyLOG8 zJD=zE*T2W!@9yQLmtF?o=Z6k){``4fU9k0|H{RgoS6-2*Nex6=KXM1AP^?=!$mug@ zxN_wRLqpH8W5*7T9XrO{?Cf&dljq*P{p*svX6v)tSCMIXZeeaWX*B#3Wb)nlAt~zyYe|Q(0j9v)eiKyOX75 ze)WrAlE>!Fn`Iew5m6lR+UR$LfMmQbKt#_M{WMX2Xl`~n)TrKp_ z4!*eYNhZd|rJh0oD3r5+7Yz&!unL^ZuvU@ZIwFc98ALfhXwr&2vNSF6-GEY(c|Q-O9Rdd; zG62rm(&eZ1`TwwY{Nm-mNpdlOQclR1~SlRy88 z8uEYg@~8X;)AG5Gu86Rr{2G*WE~4NZV=9-c2CQs&a3Z+Fdx{$5_cSAgW}3FjPD|9P zA3Dl%i%5ze-6h;&sN{KASYRHEg$Lieii@lqd`B2isGLO~j}t7IPN#@SbV-!_yO;II zWaTW|YMnJTR93Z3+AvS$^4W_sMrDpsiFW&Le`fX}EEtBh=qhbv<63M}hGB@I_nzc; zF`Wk!FmU1!1T(>>W!gAIb*Vd(q>%OfF(~A$X~k0Cpr)%s$W{%`!5kjFegm^#;1q^A ztk1v)7!>>hZcZR9c=Gru>Kx`2^Ps?QC5Eyl?A*K0_itZ$_~0E^))W!X1ygTCy1KgR zupvM`^9d9o93Dde5F=nD8DLDmsiIbRm!rpsQ{V&@E=RK+cG07os%6A?noAldCe_5)U;*SL*mHb6B z+c3ZcMfzvai>~qZvvd`bjuUu{8%FN$?;|1+VjihK!l<0OQE(nmfDR!m5@Cg0e}3Km zzp*#d=)!C=nZ&l^#`YGQo15gyL8KF(k3oz6Ps=~R_y<=tr28-K>Jaj5H4hejFCMI4 zym%zOe)f5OHk?%veHIXAm zc}78rX&)TAojzJCdFUza-#y;G`OL+|HAX$Z1J7hK|50^_0^rdfrno!y@$qqZ(AwME zL(7tgsNzwRl}I8{PBbp@#F#5c$n^fIOQl%CxH;p{%91pFTqZ=MD-&bX)A=Upd`Rc( z4AZgaIsYa^=YJ|uH|7fGzuNdK<%`Hz3FzkYO32i)#oH!IP!S1BHvBq0<@(PxuXlHc z& zvV>eZ@y*|%s5N-{{1pL%fc61sL~k>`eg8o~34|%g|IST4QVM+H; zL`f%Jb)3}&w5TjMUlx%+gM3Wo#S$#d@{d2>E=k!qT_zH!(42~Z;{eoZB_hhs&JHI( zPdGa}V|#l$TuX|gKvl;H#)&c#KvgGQ+hUQKt0FVRV%p4xjc3B|u+!<#?RJBByu7^R z;NXCbjg25Cftjacr!ZmZm0ZJg5 zUp=g1_zK1Hzn>KD#y|GXcQmAz z5>h3CJ#%i!Rjxc4?XEFqn{9WF^wUf{jAxCLS^N00H7-_~yI zj^%aiGAE=P2p@hg-H3ej=#fW84#V(qH~OZ|zGOfPjc95*O!?d~uCkwf4*3v4?UB`e z-#f)akE(8UN}^3hWc>SPJT^5G@^B1D6e8#wD%s{&;RMbWtC1}{zNx9H2`)Dv&1{wd zDWV^>LN%|k8O+ee{v>y@RQ0jd*q-4MZhimDSt>I-Tn^~`kaSelnaZu2e5GOW2oV5v zzjEIbX5=g*vf4jT^k_GAM=CQ5JSsO0DSp?kavw)(0B@^h6sQ4f(uo|m3yjDKQ&Rym zbflvo4p__Q?|MI!lF9zdBmJdjX8#KNPqE~@M_Fms^5_F6*oSRx-MJ%V^h=rS$dNe`3xW>APwod zo=_4OPpay9ZbUi+fWoYphjoRNGU9?MXF^1soau-u0u49<+f6oc5>a3XhD=1AP}G?0 zCi?b?ct2!h11|gIWJB5J+s3g34xO!^pV%*xGTp`W{%sk=UYUK9Sp(=Ic+2ctF-|sd z`a~zu95OqhoW`52zVCewA?cB3rO^nWj*bj!Gcwwvzn7tHW=1LP6kSU#arAlINP0cx zgvd?WrvVuTS#?;Qo`3=b@t#yi;Pm}_M^l1O5OWI!7?>a{#pTd@Ndb}(zF%&~>IK(qxg zy-6+MO@ImWN>QR#gObVlSq&9Bi|K$BE&1$Mq|=`fJJ>y*5g_xH=<^IG8%p{<`R}Or zH^HVob9j}q$CCGS~pb+yK zB+ds7AieI#DH?44ey5bz&pu`l^F}raT7XqkGa>)A|0*#jWD;y_4>cR2si~>y6L023 z)XmDv%XfJG{F!fyw$sy7Z+r9Rjc-$x4N(Kj^Z(UPlXLcMvvdB{{QtvR^v?)-`Nhlt}r7+#LHnw@p`ErXHE$r)h?JB znJ63nPB}uLhQ^)P1pFl6+}uCBoDJtu_vb>4o4JyxLPYDuyyE$LPRM$BH~n}RY9{2q)Xc)BrlzI?QA)`#L?1tXjC=R)md5hH6nMJku8^2)8~~6 z*2wt%>38_+**WYV_Rep$i69E&-%Pqm3klu6gcll7YM-J$hy~kl%CqGx+o@<)?9H<|jYg_9di^WJNkacu>U9K-I-CGvmEfY+gUU45 z<(XGp>my45ee@8R2IUG!4`9}M=~1?y57@#@`sw*wexBu_+$=;XQ-2@hED<&O%#2`_ zrG@{$v9`)N92*QcPg}#aPR?;&6yW;IFvyLIs~(b6ac+IJfnbD_f_=SyA4)0C)z-o| zN4)(+8J<0!;c{eem?}5{+8Wv9S%*sDvfroO%xdU7AZXyO-Pggl;l}-B;@Kak{{$8s zcW&BQD8q4=?^y*MD^)?rN|h>As&u<`<4UnX5WrfC?d_-7*?ER`yUh#?DQgTm9vmFt z{QM7&kB@M8cvw>PkNOYp_AB~xbo7B-H*T$|xI>sFvA2rl3lkMHeHe%#!^GQ{4w9;H+Al+I#+}gr}%}t1U09h04H(AC2MyGRvcn~vNFzR-( zySvM(@J>pI8~|*mOOll!;JbPM@e|x@KZ1oUd<5)tI!%mTPF{#* zD{pCO85fo=vK%y9pNKC`nI-NnUqJHTUK5a4bbbx=*I~i?$zYa zmrER@2dU+Mm$c)}L}~4vtf_WF)>I#ZS5r+j{jd44xXp>%$wniU__-JbSOfs9uV2CL z?tPZEHW&;B9D8r?E&u3wo{#~F2qAptu+m!PFv0OcU8EGeTD_ev)R7UMhllSS4hwyYQDFZVX(`<4 z&xY-?ZR7wlhcRupyxi7vw?@df?Xn0{eohj_vXqUiF5{EuB41gyeMiTkEH@tcnM})P zSqM2Ed!O_vt zP)|qM_Dk}PkB@V_l1NLpO_Vkl@sY*X2Lrh{j3m_Xpbc)T48{=UH)w^ta{h;TuoDcFc6}wn)iuDE>=X49-fshr&H6DL*q8H6 zzFAKvyOdY_?tW1eBQ@Y|D(r31bbH;C(zcU|R&(CZco6 z@de=c0GsKa45Blg{QM|G09bZpW+DCimWGssOhX#dkcKquC1KY+#Tqh>#1YAm?Zeoy z*UMMj#0Qp{K{lPLf$)u0xr_2O}LTBSKo2Wf?=r8PYdnVi(zL z={j_T5E6C5j3XkH{uN>7E4s2r$9zNDzaN{Htf$Ge*!sWl!V5&XM1?fcA3NK=F!ipx z?m9gG{PWzOvS(kXEn%Lq700yh5lJZ~VA3m)kuf5&5*gW**ZcH^EDi5FD91tGu0`Id zhOpciNhAc%EI#|$&k{Dz8K@0#`NAu(dGlsE#-NvNJCk_cND3l!(qXZjfP)UJ2imM^ zc$OVAz2IhCUeDF`AkV+xd^*z+Qee}jO}O~ti*>*2;rsMVPjyqoZK4w4=Z5fos-Y^W zP-DD=Z~r_BDP!=3Psuo4meQX>1R5eHB^|TVTmed1<%s=wBrDb@dBy&1+u}ZyeeRMZ zOPFy`-AF|ScH%xj1?klaX)k8VmTZIjWMY1Xhkqk#Yil~+e<91kGBq{T%+qzF z&tt`PRS6(+TF4)NWBmoTmHbgp$QZovyxA5dA~}vO&YS6L`~DbLSJ#A^JSMw2zMX3t5%j0oUQy`3`iqe%SugrazCMswx2qDuC>olY! zWE#?thBTyMuO%Nf5Plio*_ZW*d=o=qFrCZ*&H@-4S~9}hqLkS15}!QqD=j;pV}8f|Uup`=^%g37=& zl9-_?RMc-os2ST%j>9kTA$i1l- zU6O5gNxZQfv9tSa$l++8mc9HkuDSYZLSJ*g9oJzwDa6i_kZgDS`0=>n@+)w|4L6iX zN4BZK>+7%oIxf5Ha{ky57-`#X!TB<-Bmvo3dXhw_Oh%eEQqQ1iK;=+wPCz53MrtWo z0b#t|x=>ckTRcC%T-#wg=bbknpS$ovl4o|tp_7`6F1ZZrHf&_Qf{`4HBXn>j%rgT> zX#*OzbRZ?dzSxF|i8-I1hdFcRQc6n(z~(Jm+28t&n<#BnjQ2;v(^nFbV8!4Vdxe2*!*(eh z2TFkkiht|FawNQBcKL{SdkB)_ifUZ6DkSV79{%<$QKK-{z!rfzZ>wpQ5v^*-@fSR^t>s~ zM}kQ7*V5jGzWw__SztTH24!d?l4A&ecx!EKHCh}t1`HU0=H_i|M<|utLlY|~#${>U z+{e>6@*tAbWj`b!Eim5@kwgFx%8DZjK}sd1AtfRA*${LCwIfI!=BFVIY1jvXb~r;K zLYOazorRlkz8OzH{S2@B_wSFJzIzjY%?{PA0Wj7`5C{M$?O|JXCGXfVJ6Yz}?eIE+ zQZWyNw0mWw?kKskY}v~tiAUP76vjhi7|)qA2eW6N6A@yLofs4I_HuAOp#-Dn`G!Dt z@zIZdl=u2&ne)^0rK;_vmtKmKPC60m)~$o*d${9{J8<^d=itmU&%~{_{ugq&j)<6C zv}h5%!k+et({TCamt*6`4P2Ms{`NPRJ9loWP3E6)!in_gn$PD+(p_@NCHVNqKaTUy zKOZl?_@a@jh$I~LBq{&T1jql?uYQ%1iG0D4A_m|0b>0$u?Q37-e0SE{s$?62`|{FD zFX8LgUymEU@r@FA{mLuLDB)x~B$T0~>=rVP()bupn?4QKUVANWy6HRYM}ya2`N~%? zefsoJayCJF@7Nlt?$Q!uWUOpuGP0~a5CF`%O22`nq4G;uV0-X0t?;v33F)>&*}@Q! zMpf9hh)l+f&Xr?Xm^pJM)l6+W1Iu=?cKrtY5mfJa?N$3p^)p zBo{3rQOBegg~UfbdLrt2^kRR)m)N{{i~igD94Mhka}&@|Avk{XImOTZy1E{Qh%plB zBG8Z_l9F-0&`^*vKy6JOKhLEQ#smZ+(qzX1%Xa~ifRP9B_*z&YNC-P_mNxv}d~-Se z{O3R8vBw|N@nXF2(lWHS=c4)+_?Za6CQRGiLLsqVyNDfcPhnU4O18oCU}CQ*^x5eN z-&cgr=ea3|RER-o2zrK>tefk|L&+doQ?_j&C16<&)1^?fw^-gE$!ZzIpcG>81k19I z8lJZz+cM(``(hskiT(lw-g}C z-pJ(&oNG89uA_?f5`e>d!sK;*A5T*nGGLpr^$TrmN|px^s~aIYN2x=$G`#QNrZ&nf zVsBKLQ5v8A%mq-ecgdlHWdj2THeloWwfN=Tcc-=>(~yQV>{V*nh6ISIc~W}L&6cDE z`LeZg<;oIa$NUd`;0UVqZXdq&tsC*+gTJFBVgLREHF0qh4j6nOUvjcWZMR(2n%8lM z5^UF8Q_`V4m^!iYk390Qk*Jf5O^D4UEbBTh$sjuj=ve2=U-=5v$}G#~9F01yCf#O{ zJo81dTQzpO!Y|MY6@pcvM1}TkRH`1S@rG*EoM(G`hn6CJ1P2{79D@fBj%?(8?|a|F zg%^GfZSM?UfqO=O<@)Pu} z?+b>VSYJ``i?`gZzY?qX{$av|ak%QLg}Cus-^79|uF!LyhO$7C_okb^TXNt;r%qC- z$Z?91L?l@e^`D#`$&OcFeg!vv^P5h$X)4nfF1S)lFW$(uCr+9es$lN4<0+ve z_kQg7gEg^y8Ls>KSDE+aYrl+f*JexoKF|Kp=nxb=FZ>K8^cbTb&@wK^8KnVWx??-2 zuB}63<5oCMmX2!_hi-LBm>2RM`x`!N7=J*c>a}BsYs~v4FTB7X(2hLnD8_oPnC>Y7 zS4dRV)MCTx)#wMCD$|_>WwAxtdTiR(jN!wF^Q?#S8$5U@mM>oc%X0Xg#rFkGCcN|R z8jKz_25zQ`j%jSyL2X?x?eOVc{+M99+1Piwz(~I83=M~nL1-V7;I##dKL)tA#e9y= z{DX$A90Uqwc8VRa7{3PuP!{jZ-zugva&4RSf%vlu29?UUr6HAs+!un>$Xi*o+qLL$ z>WG}%i^yktqcVD;1y$%hcoe?)rLUv^kkLr3Hl!g9Y1m6bLLCxGXAg#)voU1IP)g4o zdgx)8F=Gah3{=b2b@-)J6Og*j5J;&^Ujim1Z0VHSC3IbfX-0~#+`&hzylInt8lr)9 z;)tgSjsc7(Pnm)*{pWv@Fg0r05dnG5?AdI)qGXqj9xBRty+b9@4Z*6@Lt9?|M8E%Q`JdJ)A6NX>eMM{XlS4`B=>x$ z)Xn#_x_f2#Lg3(u2jf5Wxw`Sj8!16(oFwu3nQqs-yB&FgQ`;z5BiBQf#1CS*I zB`C%_2-e>PP{n=IrY)3soHlg^=hmXv#IZ9NKXDRjduk$S%{r1rf73+e>u3d=>MAue8+vTUfdB9V}hCOq-ASla`8o8f)};ibbDfmnUQl zoY(FH=jcln#vG@TlnCVYvX@`QX1$(Kx}bETCcK;Q>MO4^P3I9(hV4dDiHvoc* zKKtS2raTtjawp!{(g8nR)-i)l#k22C-uwQ zXFf9zpVp2NF1qMq6387L?V;g%eO7M4>8GDwA}TqCEnBwWz4zWLNug5G^xS##(6qG) z`Fsvii1$I_s@;d3w+R{9VjMeW46azP0N?%YcPag-!Rt#dxfo-|j^X+mo0>7Q9YDAJ zYgKldJGl7bQafW4j$EsY( zb7*SU*H#gpUoevE1OkiCDhbFhe({UguwgxAX^);XWTIT|tmY&WZsfu#}H4g_s7 zDv}HIFbsvp)=j#v0_<8JTiY6G!%$i_1mZRaNsz7EfCX1xg^ioHuG_6t5L55Wzng zh#{8eWV6`T+=BiM1EJ^|*eR_w$J3rGpikd^*xa<0B+=9vhm<^s)0_C3^&2!{yqVJ8 zed_z8x~2!cn(KM7UgMUnIB@V#-oK=!&U^Q%#|bB%jKhySszhpX9XzRsS6+Jqb-nxG zq>r7900lw%zN0^MEZ$wa4jUV{Kys|Wa!q&*9XZDPM(UlH0dSH*aO%xa9+C) z#)EVa?p=KUsa@>q@vSJW8P8E*J6wymR;+}R$wp;OOvtbe+B-zl7tdP|FJBar;JNW7 z_hs3uuc56y$F-et=wZkgidgZ^YUGMuyuWzg6%fuT&_LPwmfVZs2aV)&^q$_!aR*Pr z@R1|wCBE}Ec%2+WndGA~&sX`gsu&Y2ALW^IEHsp{Ybk=zwxrb8K4Hufm@!18BjE*r zr)XP}f}k7W-xuqWXza41&2(Z+C64WCc^s<95w?D{p)eawCRWi zYwsW>{!Kz^HzN&c07F7bsm%Ubc^P9PPDS1Y^}U7&>ug`1m^W`;C;>@A%cwcij-XOq zd9{@qY%}KYCP|XIVg2*b9=%U@eZ-4vSxN_l@lw8YJRZ&IObu$q3 zFlr-7Oa=c+h>zKj$pBDOQ;S0mnSuKHzI2dM?n&7Y%Ou6LY8xGq{Pd?k#Seb)1FB7H z5}E{5zkc=F+xCa?!yo>L=^AwIGicEMT>Ja)zrR#pmpMMPN2&YkhvCBy!vFlw5AmZP z{V=L?yly9-&%Ym%b+3X^S>s&wS)zT-FMa7tCGdLEq{)%9AY*^jcoeq?(@vNBX*)Iv z!6}oc@N66VrG#;|r2Cz~^Q@nvojxym4>-OhOP<&J{C!-kiRGzNrxN;_wkMaqywrG# zSIPa7B0)xWF{jSb5=3MtpHn+UL`sEnwe;9?tkUlSX`qClfeEq*Oc{Ly24oMye!WIx z{D^6Att{fkHEdV`Rr_(@1HZziEsc=oN4(i{=3tIS(f43EG;aWjfcL%>kdZSAo05=H zIG8l3Zl8*QP`T=ZhZU$_s3geTr&cZo!J_V9;nJQ4i zP$O5&v%jbvfY-hZAPor>lf;siPGbzAY!r$T`GSBiP`YRAX%szyLJ2;l_rQEL*{F%Vx?X+Qhi*4?GPEaQ)-ya?U`$esX#q*no_*>mCAzt> ziKOjoZ@$IxPo6d%5?x8?F;2{hX$rA=(wspJy%#G;`p!7?aPCF6rU%EM*OcTf>&5Ex zI$;yiAWVyw4-m~y?;bW zF~PE?e00Xk#~bYj(Rn_g5X2;p6;w(qr6DCD_oYy!G4F&AV%{;+@tNah;QX0~&6w(Gof&Sfx}YGLqhoSFe5-W5$eTI`=gmXrJ@``|rbs z4I6pCZQE8#{wSqzq?TOx)1RKwl9X>md%tq!s_$(p2PD?%V*_bkAGNcfMzNT8ewi`-`Oe8~@83jMw zQ$vu_i+W@~&4BmiX{L>Cy=0s8O5`TXD>`Vk_+y1-rC+zx5ZPMQu&EYHJ8U)`Xux(X z)LHcynmG(}j=UCUOuGUDgDG$ewQ%hWRJ=Y(f(tJEf)+YBjAv}l+_^aGoU=(X+M&Hb zSSa|ST+PyuFp{y7HR!Fz;iJ>Pfa9lLfO_X(2)_y*$wbBS1g`_#$GIiahJ@qkpF9hv zeEbuXPIR&vl3<=tApsgmD(1Z+@s_>3Y2)%^+~c>w#zy?rWBzj;@FR_ zW5y5GRg~JJ?LC$^(q?-a#vW#dL{><$>G3djvQ?Dc)V3>)Z#*m04Q_VHo*Ks*ae<;+-<=PrDvQvm4>W}-Q+hAH-N1pRswfbGGT(ug*MvSCH+;b0Req<(JY7aT&5K5Uy!Iv7UZwe_Aq+!@D0QQ{>Oy7PT zQc21*>~ldS)uAh%ZS(9CQVNof=bd++mdv`64oec=ve|BsaC(!-Hwc5{*v5;qUrsE< z5Rx?XTJ5-ivFDW}BjN;)ky4{npZ?6?=ZDGT^>@DWo$zy*^%LsXLc}0s_mGt!DnND* zQ3*npt%0D^l3BCP((2Q5wRGb>m_L6$2`k_CnD2RB2&U<>%-0^Dd42WO*OUk-uKToU z)3hq~XSB!k+cEdtxm4GF;)y5N&aZy;E97!{t$uuI$1YS>A zh5kUUA5@_y(?lS{G6tYTWRIRb@%b-)0q31RU#nrC$#M#yN-OKIUSK3J6(VUVjyy0# ziBlr)Xb(}95u^Bhm?Y}>U9yA>5H*M7CVGTSfcqVkr zwXj@=ZI!pJ0LF%N+7z=Ol+r4G&-2B6>9Uu%_mE9v{;F)-?Jm4i`o7Ot6OUTb z`yC(HVY-7k7{k+#qF zP)uUs=%bIO?KqaPqOq}FoJ?gJ5hDy$Vvh96ws`Sk+;-b-CC3-EwRPWp_u;hDP78@c z+Bs`#YT}%wkl}dCuUR%px-dO_*`_+DM3cW^Hy|-B-#AP!&*-+`VKu|YkP$JQB0?^p7wA}XDdJY;g3k{ z&YyoCJkO`3{att6snxE3K*_n=Zo5@`RQ~|?NvmU%%)IN)J9WJu;wo+5tVdlP$Lk9} z1}SB?5MuX2R)COk#P^TuM^asb*Au2o#{o?#uGh9~A$c18q7gC|kg?B>){;K}1kW z8-%blY#Rz$1j+`TRY(O92*US0k~wvCy)g5`GtHT!ok^YUMv{zi1Vls@5|nu2bzCY+ zY6J$9UL(m_-J=di9QFTL&UUj#`l`GS6;ppD`N4bInWL1X?Ph52FYMp*C5h9#N1#AN zHXun-7K)UDWI3(AOuK=W?S{@s_B1%YUVZway0!-Q8>zdiCzBo!%_LmTfJ_6+NnAw{_$wotfD87?`#v!MRcm*>R~-zB#`C zjP=;*S$g!?aheD`RPXZ^G`F@>-QANxC}GR>)NSw_buky79n&8@Pp{js$vAMa*^fR$ zR8!Y$6*_vM6yw(B7TW9#$EMGlPHQdo#riU`Z<&N?hvz`ggL5u&ZULIxI;c87c<8Xw z-m{;|4R+TIgqk{Ta{Ko%e+xu9DuhP08drh^w@lXVunmXc7UP!WTx; zR0;Tj)G#Dv`sXYSDG9mn1tEO^0je@tI3(QSu=HOOFkrwyY;9?V zPyj0ipuHIIvfDuNP$@-HZ``+jJb2?(i5q6xWIfvB*Y|>9(?dY);I0bi>G!PKqL41C6nsf09ap-g)goj z{NM-q(&_s?9Y)-&?Swu4_~SVK_~S_q3L&Ug`P_5QmCBIZd-TyqOHK;tS@-0VPiA|e z9Vl(*io~K;3#Vk`hd%TnO-4Kj!~@CIHEY)B7p46%Wy%y1BHEiP$0UWsx#ymXwQJX6 z)~s2WIddi!{c#b;%6^R6=hII=&3Sbee)F5(;G~mIDvg0sLMvCUEF)}EX^J%Lb0j3u zmF+gf1E050#rnA8jxUjsEO*k0CsBf`v-TN`G!msLO}jzoG1Vu9u|1h=PsZRlc9~kY znL}(&lKm3w-;Rh>V^5E>Hjvj*vGyj0G**B7{`Fx>F6kX(i^Q~vy2cjG5^ZmvE^=s z>>dfp&E)uat_ZUS=NzcNDFtRdI`eZxq=vTwGfR2)Xbz;zrX$ICHP@_yZ2 zeq}N;>9l5hY0K1HN*XE#W$}Ly7Qys@un}0`wQXCx_iYz>w+36SUfAOFMT^xJ?HbCh zMIrA|`l;ZHP@0hBW8~2{lzM}Lury%72W=hlTJV~n=j&o%BM%ojRHIq3t2-vr$l!hP*(${H{ z@B>A(dj+QF{305gTcGS{4muxu9D(DaxxEA9Cr?IIT^(N0D#TCzWiejX1f-p*!l6eT ziJa%7HJ``pZ!gENk)wETWwSV7*l@hQdub&63@T1Y_`+f(Sg9pAg~;+Q77|cS;A(_ zb@UO9jrX21*9LwBl97x}+fiLYS&)GZ$-EF|94ho2?m&{Tq-{z>!Ad{l)39T~O_yNr z4w5V)#jIh$LO@Q@qF6LN$pW;uaLTVUb=J{Yy3H@zA4>;M7l?MlwPKilX`o(EIlszH#F>@k=dPIc(@q+^R{@1y^3F zrLBI=d?%AhABZ&k%iDx37EysLAXq;BiAC}#jA>HaDYh4wHvE`0CSL+Ob;wgF_?@bU`+j6M(hzp> ze9P+8`!46cdxFHS2FD>;WyUR{^#^PX!8~22vR8El*mn7RRdP>Eo+($J&wdRN7c;Hl zsPOy+iS-uB-^*>wSrtfr&MlmOVt!Jpczd==?EdaJXUTJI#WXBJo6W+Pv1dfWkoyh{ zsc-5h+cV_@_)7BNC@GMo^Bwk~VME`-(J|Ws?*R?PHaJcl^8!@+3n(rETgQ%Vv##ln zDC?i3Ei&8B+lk<$A^`uk!4BwHCjJfFQjvrJ^B4pu-BeYDl`X(+zkdT8n~NA!m%*$L zPr!tJwWtmX5I&zB6osJllrN12ky4aWq|_1=Ne8fP7divV96|>I04QBo5V{>yp|Q=z zqkmt4tt~|~^sdH-4jF+Vbv<-{?fkpOw(J1Ps1(C-_*hP(*;YSFgs< zp~EQA>3N=Uloe_wDc*->QmSfd;7LW*)!x1PpsKDX$xJW6`v_cuT?$qJl7|JMaKO;v z7<=#}C>5C?p&B)8hnQ#_gDQaK`wiTWgp{5$e#IcNlt$z;usOWbRco&eyNNuNwV`~!@OGC*KiyE^}!3$#Zm zIreOA9kM<1cufb$_eZuA?@D>pQ_h)CTA=s9Uyf0B#r_0+m(^LRB8a7q8!8w^WG;Ho2n=^m+Ev}&g^I=0L!It{o zwl!Q0{@rAZK*utGBeMt`AbGv9Z)2$Kwm1*AhYWLZ8iMNe(>lzymn#)Kf_~)?}+n_QdGe z;L%4OVI5tL5{uKOOywGDPvZ2xx?$r+@Wlug7|!o^}DOP zcnOr6EIl3n``h1g+_{bp61JzDatah+>_58P$3Ohx4=g)+#7O?J{q&qUTHX5uW^Iy;hzQ8ibIw>vNfM#_1&3dLZ7Y;kBq~ad$r2P}^YYOx zOgmkx-%63Vt_+slOTjr*%G=TTRbHoL9=2?%VF!|XLmx?rCom3Js4gZutlR%j)aslxB zzr2d&EiTrnfp{m^3lBW;I(!RIk>sQe$Ig&+6qJhA&>6z0G7BSVR)h^t*(e4Y5|kFx zF_H76PIpm&ix=KrhYcN7Xsa5C^@S|{`sx}KtPDIj5COq6M;8wv8EO-j<3q`pr zNVeD1(uOU~?Ky5mx%E5lx@aQeO)12djy&Fe=N;7a>dpTAz=5<}#!2vbAb1@>_r-d6YvpRR z!qAKm5(0k|Wbf0Cb>-wO!S6@B79uQ!Egg^{jlfbEhv@_tiV4lms z4r9Vs7S9i%0*%;Oah)R=>Bu51c$R}=*bY^-0fOd?011oIkV+#6Qr0mIDG9mH2VY8V zngT_lnKonk^wP^Xjs6cAIuvz1dX!$aH8PN8U!rwf$uV~HCHl@g@1#`XQAZs`_2ow% zd4ww2Mc<3mzWUYoz|=lm(`*UAvAF zgep3mjM?X+MUP_6+)v|-vp$KlXU!r3SupB$Ws(#u3(kde=e3rosDY{#tLAgzx5%^SXfrx!2gyk^gyjn>vy07=P*S!bPv=a($O z#>UNf;>jmje*D;RoEOKf0=BKaw36IB;;18W)X_(ikgV;|LyvJQ$E(*qeE`yshUDiJ z)v{&$^Mqs~^EE$?3!oBvwtO2`V}{E3S5tK8n69QO{L z2Wg;!fY{zcy^((9-(lZRl8m(bDFh*eM3SVGF)!rCvpDmmkSWoahJR|X2otCZX_H(K z02;Tpl5ljYYf#KoVN=eBWa0nM-u3)65k%o{XMeZQ5=7zP0m4B!$O+|+|Brf*e@4#m zk61FAZ9A0 z?To|q3b5UA)K2R-C{++#1CF{DrVm4SFs(e>_76^QdftTX7hpPLFx?y-rcG(y8P`1s zZ$MVQzrR4~6AGteLqge;XhdQl!nTd-SsgpO-*MTLn44cf^SX_O!c`C>5-w_&0d~Ia zBQ_MYoeT$t1EkjU{JPU@jL2AS*5Iu#&fYCqK5 zHX2;R$(gYU6nc-RR zL$yvYlZgnpMHD2RY3e*kQXOYzW|A;&p3}IswZ#ZR+qO`toFI~s4$(Gve&NfdB?v%g ztUiP6+!r&`5kmyJ%xUxBIp!%UB(6t@v92)|j z=i~kADjl?YySsR?xCmX6v6=MX^~wsV=xQE39}@sV9gP)6A)T9ZbF)}peuZ~$-U3}t zW*q@Zss%=0dPe1eM0gA;!|B+=_bZFp^V`XOm&ryOclCv?$m6Seol(xEQYocFMa<0W zX&SVkY)|QHBh$(-vf~V=V{_lsG}xiER82>t9s}q;gY@{9ogr!RFRf?BF{G*4l<6s1 z8m^=*5@ZsLNf{*>$}?khB_`tiuC`3ApEW_5|G}>PHu~%Pq-BTC@Am!OgWkOEqmAo# zWT4KLW$NdZ>eK&^O`SP-cW*E4JLWnPL+RV;_}W%s6tOmi0NYKIdslIyi0SER%+Jn2 z2tf?Uj;)YcZwfI%nbDu}QTfB(we`qRT;cDWs-Br0ukR*a+XyFw2oc#vL}7^*DG3H1 zEKD3b5m9&vzknd|gt!S|2?LT7=OGsoh=jx=ih`27n2UlVBpY7h2+2I*pf~}C5RkpM z>F#r~{i&w4vt#diwN|27{YkB^ov!MtuIa6*`szF9ECLrU^7*^g?zXY(hHLTuJO4l^ z3@wz{@x?I+DkuyNOYgTqV4`qK!6u-{(I*}Oa|KbIRRA%l2bcgZp(Zl7ZQY2o`Df^1 z8d(Ic-gFi3H|vpgLV)OkmB3;Pp&PNv6*Du_{Qo`k&wr8R50Q#vewM|I^Jo>}oV(rW zP>H948YZ|TsN=FhLcw&VKS4VvMXOS@H0_ zUcY`F?-5!Z82vylxc|aVj%|2o*DA=!>j7>G(*QMrYT!7c&a(VWL8jt~ql6*r7YlQ&_&CuPTAtA3A)B}`2;BXGeAx3CW5^~d1Q~3LtGnFYZ zuQ7ij&s2m1xrmTB6O~I*7SUXI=gyrByiyh=LL>=?ef##Y&WVXz0f4H$AKZ5zcFsk6 z``sUYkH4Ni&44a_V)6AqzDdGu*NrzaD9i^Re8B4tM^NQ+0JR#BWZ!MOZ^MzF9KkEE z{FcFE&Yz#8Db>`}WMyVf(r`BK(X9L2`Ez_;Ri*0kPsfh2{a!k<5U2#rdp(kl`wtwz zgAYE)0605t_%cqs^EM-X|M33%0f5R;wh6-g%64fL%z$ZE`r%}M9)J5p^@G8-xEqch zeHG@8>#q9}7d_qXDx7-n6t<3y)u!R!zxQ4|@x&ASuAV%25GB|~PAF194FDc#xQzg-AiNXZz;0U~rA|Gg#N|VEfh$n9XNVFt4RK zvO{3F7#vGOV?Y>Ph4x4r(#x@WE=`MsZUHkh$lQ^ob#tu9 z&a~-NK|39uC-WT2qPn2}ii`F?+a<#HHiS{|N!laaROrpsCn9{Gqgm>P458Zv1MKxT zZj+k-Vw-@Tbx>vzMTjU=>2tCj44E2&D>(pmu7OCKYYR8x(q}SoJ&15zff+iTHl$js z21S&B;Lc~uAc+a~daY~CthYf!LSDIuF@pm5>Cb;g0`lhZ@yahDeGQI`bSn~!#Fh$o zKqWrc5DE0Ai$uOqlBgC)-1)e8A-?#8CfxlDlI?=I5*uR!63h*igB$lmAoabM)pjg6O;4#!HcFw(%sDSEd z1t5Ua#gr^d|urP+LSKaU|bzX-wr33)**t>jl3BDznG?&U65;VREMA*`$p&6i|b@qe$`my-K^G5%)J}NXRQ3W(HTG zY4lxt_madSf#>vZ#3<8&N+G}jsAj1gkmuzkDV1J&=_L;MeHCPkkwoN{En9H%w4 zi0jv_!;3GzNY!I2X6VCMUs*}nd9R|#dA)?!=EYAv^%Tz)?!Id;j|w!izU6E9&C4$% zp)Pmm(9>|If^cZZ<*JRF@WKnfhN#rRdtQI_HI~h3+O2gx>2K!iA^DjVb$e*_Ck2&{ zA_hyXCIwjLoDT;NmqbFgF^i7$Fd~YsX2=8xX6y*DTG3fUinJjn0t7`XnP(5XN73Yt zWEKc5Sk{GT3zLPzBG_6@rq~dpn$yKF9}Oefpdlfz2qYyQ4}q#M-5p&GgW~+;BnJWx z*b)F&PJlyj7)dfg>C5rwL}lWTf2}N;b(e4iNZ(fr(-JfEQJAqz%rfq>Xi{$)0Djv<&lf(hi5L&zojaC=O0a$aRGImCH@SW{c{fh3saR@z{7*KWOyPl1kOXd=;fC02=Ge_vrJbIyxUm%w7H;&!69Vsk%3( z&kvcCaW`&yhT6F3rP+taN5YEtA8ixCFbr;dOgSUR zADO!le*~iJ@4-&zF_Ke;s9>AFg2$~(2{$)47{}4S-@}W^4Ur`+h(bdlP)L{{lFUSgb|g$t6edg-VTNc>7GLM5@1J4^AOm$mMgWAD z4r9y)z~t>}tfMCcOVAiMhEYxg(C9w^PKrGoqLf+wmzpw;vF=j#yQ6o*gk>@2s-agh>)bqGlpKJ0~USlluA%U&Hv4xI| zc3SkyioBnH_zv3`E9-l3rl`*stNT^kuPN4k@z4L5bceFJz@7?C22~(yfq+OPj0WjP zu{+%e`Cy0)*ubBE{f#&Ky?Z7;d2#_&J;oqXtz$dv@(yGHNoc|m@00rlj88c={Rsjy z!+02wMVl|Stm;3=l|a^1-6!eyp^EpRi!~f=)~`G+V?RsTRjDbVrk&TeTNG`(txMIX zv4`vH>)RvN?kVy6ufO^JE14=)JEhd<6LbS)=dlt=D%vCu3F>X50VKo-^&#qggrF3H zut7h zRe-*h`dpy<1=~HDm(G^r(l0VJK+YiqoL$unuuv*zBe4 z6V%2kxyOFx%=*Yj(qE#ki*`W}^|EI8y!*3h8L8MuCBr`*^+biIF1V=X5(4;Sh&2r^y?OO*N0AL zg*Jn)K)@AGZll3Vn`1yS`n`TkPGeTQtk-WD$H|S5BZ=LRwz+I0ivRKuVHHgzRAG&dXEb`0HOdDlzEOJMNCOC`pomhSJd{T!NLex z6oKKKD6xw5w`}JTqh{~e`wn#FE3V-~>Nc|86r9>bFO`%924bpS#IXg4|^FF`X{hj69ns&owp8VeI z-aWfJyE|KEo_XH)g@!}hvSxg7`z`S2X^UW>VA6IrTt(YG7OaW>0+h+mb z+k@6I_xtd}f5s`NoQ&6Ado3G4P)bX7?eg+dFn;`ad{7Vt6;{9+hI6*N>#hB=d;eWV z_(2u1;Q0{H>#&i}cPv*Kz=)ZrXr7yx!{eMd54Q8^~I z;PDA86x^CEFL)va^!55*E6=G`3iWk$5Jc@Mh+UwN0s;xyM`69ey9GM_t>Y-2uQmr7 z13POFipFK$)`&ZHr!P7FmM0x$K4%{m#Bt`*8Hj*@;BL)l!DpH+l~QuC!%n*~N!8z? zR4kRml1>$nNh#r!W+O2srt$mf;P_Owk3II-11Fw14x4PUN&4Pmxj*^IPq1*|0{rfG zzr%#vCqQe>L=@JVp1GUnNtK*+v|Uf3wa)D?B~$Ob^G+VS-)CQXH?uB00ZsGg<*#iR z2wGM6>(|+RE7zW;wXSR@aK5#*rE2|d-orF&P40f~`Mt_*cq|qB;PLu-mf`OeuGfRX zMyjIydo0~wd6@|$NI0jx=lNiZX4yJ`M;unCkcp{CS?T2q~=n}nzjk0Q^POjC;&NTID25JzrSy@*w#ISCo_foFY@^^#;i_gmu%ZN-~2D8 zPoL&VNs^A9*eGz>j2SaAYu3!}?lU*$k_8?9PgL*>LcN5ZeJUoBKp#Im<$ z;qB=!q2Ll!AR_~xd+s?LKXxn@ESN83hyQ*g=mDs@tf{L(WLvQ0tr?i}@5#_esN9S^ ziD7M49*#S1waSxia+VE>T8C0nVmZZ9QRb@#6SP*Tg=cwvMbGQszxy4MwEl}lBx|t# z3x&Ad@{CEOJeksbS!?Qk^XFt>E!)!B`L(vT@>`mgS51%-JqW>|0*w?9NXWhk)v-nB z%BrAX+q2*T7!VNfK}Dwoq_x%?=SfDc$~(-Hk2GryV?JS?k^jL>ffB!7)bzW2TFVdl(PFzJMX*$rB;oY(ORX? zDc>zCSFYr99z~H1o^ZfXE6wkEV^gE7!*$aEEgMqWug6&Z`md|2g-YwcakcATwy7#l z$Us>x-$$`1QnHM{t%3V3M{+hc^8C_oGuyBN`K`IRiPMw>?F_1mcaDuhbjW}}LI!jT z3N}=GKtMn*z{h9Ird`3>$`RXYqRi%(qdp@PCM;{AN}ZJHpG<**&Ejc-gx5;TE`tA>(zw+ z!CP<5ficWDPg|9H_?1^)!4H0LF24W$@8i{1UriI^wdWe%o#&bA%I&t>4wqhf39h{I z%9Mzd=YVBhcG+b(_uO;w$xm+IV@0hIU|Y11KywZ_}?=B30Se@o$DfXLFS;FXSYym6!u$2HjI6Wf)`+YTUX zPAxfox-T!5c+C0odWs}5nJTQMr4@q*4aUHM1F^vd8=#?o1Nt{Kh?fJG4H-P7QXR)_ ztJKcxZ#yf5f9OX252~pD{^j~_sI31Xt5pA7-c@-G4gJejFWv#=viq~l!Gi~_YI!t) z)+$p$8k3aIVQ6{3gJu6bzxw+6@^k|kKo?IRKR9UoH#Mp%&!$uB{9|d`QDFyaDIk!L zeHPaL1FZtCYa?7s+eeXSRDuR>q*8CP=?AYUQ_~D25nV5AY9|tsb zg*x=mL#1IaZhXAYzV)qdVfO4SU|e|Ng_t*Q9!ao=9{PVc^2j4`%rVE{fBxra?7jD1cj42SMcu{l8it6;ScfCpI(j^ zUU*)PnF`2@FTS`;UhmkW`f6!u;dy!mq%R{RrwLd$P%a;*EdxnFl71i}P|@fBn&mgL zY%ns1k}+s0^h3>-+o68j9nq?4@P2&z+uz1y`~k*mco?U0Gs4J z$)ZkeZ4I;uNmbQWl4Ww9J%~Zu zSZvMef5V|eV3Q)6R<1x&Y^|z)2UBITt)FuJ=Ejwbr8;=v2HYlchUQGdecGgiSOGP$ zuDai>OLFzmPFZsNGSBlX7Td70aXFU{95{eeV@loyo9obcj<6U$d>Gn_Em*#M3AheS zyY;VNvv-Sf{*N%T?fq`6M{g^( z$^D(P_hZhSIk@k>`|!jQPh!d|ue6&e=cb!(E~^tCW5ad$@Zq@n>Z`EPMjLfU>6N*$ ze)X$gA!+s1uY85J*Is+IXR;30O_OEx5R%1Ph!w_wR9e9#REf$|2sx5;N=1FeUD?N2 zvOe}vf5ev04!{`c5yvrR=kN5bi#gAT@BcimaGyKvZHhhyulKZ+f8 z*nx`J^UpsY`|rOW9(?ctoKPl$ue$0gx<>T@hyu7w-d#X)@WmHj$PxHa6p?Vf?6OO7 z-g)O?+igEa)rThyyPNfz&(_qbQ_B0fysTvXQTe)VkB@)+<7GwU4{*sPmr$|jU90^d zhRsOQ9zgb6K9>un9C=QJWTtvn&<$m9l|s3a7_i0$MN&Yox@ueG2^r_I-$-LPWZ=As zfMOJ3WuX>LaV?BeXb<1{&I$Z}w6?UO5En3S-h7;K`sr9Oe*u(I{MK0G+DTfW;Bwwr z3MCbzS=ZA{r1Ej^^X2iqUMOVcVd>H(9V!2^WuBmv?*UzGwT>}(@DLc24S=Qt2l987 ziE>SAjppWNuG5=JNUw6^yGbcU^8{}iR)X{1K5XxvuIgV*1{kRfbT+$IRohr&X#!D1 zCW&lY>azaxi4jX^YT`h)C@iJ@A=~Hfvx-40v8wycc3oZ;TA^4hVcD{!5DXqP2m*hO zsVbA}G`F-sDa~nO42BII1|7!?KfrQwW|)KuR^~f?&$uYiNdbX`?2}ObA2K7ljs?m2 z>JiawFHVK@`T+p}RrmnLiS@A{6Ha~Ot1AMsT~cqe%{FH;m@jfrpzN~K;v-8fl;D;AbL1K+XTqwvq<0XFUm~TOA<<)GQ;)9Nwe*NgS z`^|42f&&jc0Du1T!#MNIGf|1JfBox1q={0J9g9hSo+M-}uS@@i{!9e)%rk#yr{QmZ zdzy)YMvWRN9l!E^zloVMXNa1H+bX5;=9_Pjh`i{c3z-z>`R8BYy|L$>U*eJiC!Wy$?qc;OGF zu4Ean|LD;>;DQT&=!wWF@*I+s@C0Npzti$DA&s?_zlWqq0qZy^P1oKzjPocKDOhhZ z?>b0=X$_!M3_z8M!Ere4v@_rwxNOdxcgo97#De+nLOF$!#J+QJ+1{U2Glo$lA896v z;SlpTufXE=Dpwwf$Z~n7rJ3tFV0h9=C(||CC#z{U^odfOipq`eqM<{Fa1hBDX7kSz zLe<7~y=yMl>)ja7iqq7ER^X@EyHKg%eN~xsNJUxw%K#zWkOifrRAtthN@JZXrDU*~ z1a=qo&+>e^X`Dxvr>SxF6*ZIQ>F%c{P8F7=%bI}@h6@Tqq4FclXYZW?t2wT43ds%TuU9tY;+!V)KM6F z+*mm4_?r(*&IW8>!1dQ(he?zEQdU7ej1x~BM}^yr88f^qTY*Hgz-LC5jzYOEuPM7! z|4&)bc$m1_OM{))PznsiAm>@e zY>Y0d4m1PBJ(VeX%kWwXLKh}jGl>;avm8qR%#uycXUVE~6_1kzt5#s7YT#dtp`yW` zpg3A1@2d1?N@^0wC0XtW+SJlO72{9gZ^@yKo8^TOP{ip!xq+eqdGB}8%y`EIBaj35 z`|PcI(s{wM5;YqNZ6=Id8ZHW`_eldY&MWzYS3RDOOW0JZAkcMoRSWGk!Hw2r>^&$JxC&u9hiISj$#6WRZ(hpt?L#{A`7N=?ru{ zxEks-rKKKa9;e60;^N|y7-KOTMGuugRVkAABK4{4TKHOhX0-fo^vvg%C>hf=ZO^5r z8)gs=3^Hy2b^Zwa8nxm3wI;Xl@0EqYBS|{(pu@*2^uhVyiKICjuI1&$D?Pe}M%D`$ zDnA9H8j`PY08B(|Nl<~}d3q8uIwCT$9-I$qLcwJPawE1B!U!~;AMHcpv>tnyO-3Mj zSGQxfbt#XoIFttj>liCKMCo|#@!rD0XCZ6E#-+69{v1f@5_!(m!nvrlo6~{2yAjzJ zK$0z8f0*wCbC6z9V?p(IFXs_zM$u6q#R88q?nWe}sC~#Ra1pR?3;_Ofb4|;1V6XN; zi5M1=I|1<1Bf#%w0{Vsc{SwTs$D0f)-z1hh?qSPU=pC@*vzV9X+E6>E7z>~7y?`Nb z5dNN{%yCs`D@1uZ?lTybu}0vr?eD#htFhYV^E{t}Rj(xVBdlhWJ2mHZpK9K+s6;GN z(WIa(KyD+yNuztSGEq9;>Glud#Qrsfm1UUg2IAx2-z+U@z`QP$h5X9Io7ii9RL`=( zdywOA%(~;5ge)#aJC@VpPrA-j+h?^_- zbkUU{v@v-(;qD;LnfU%rPh8BZs=iFP_}$%5!Oh;x7FI~{wCFUc=EHn~r`9Iwz`aL} z{+g&|*i2PdMwl&GBTpgG))TqzozL$BDXgMl0kY+4ut>AESi1sWTQleFujO%l)@I03 z{bXdIUZUnNnJ=o?_kJtciAQ*<`XAbB@!7uXoKh^fG zy}H?Cx3uLt`uG&q$t{zFnY4=4ks56qL4LwTL79bjz{IWLQ$>3kwpC|Mm(fCt)&9a& z5->SlW~b%epdzTvf&VSGvBt<&r^!|ixbc@7ibo=L*C`O%fc~X3^ zJLa!7n0nI$vuhM}e7>4Bn+68CPk>G4dcSpHQ*p;#mis0RtmR6aZsS*FZRP9cN-7zM z;Ur*=$8yoT6`RGRn7%%P#Z-D2RnDtWrt6Y+k}Dmpu-(Vw-sCsNi6QfhtRumE6Ut7N z(DOM>15eEA?q@t&Y=AeIJ90>px@Bltk6+rnIaZ(i``7!nfY^Ley6X6Pqbjx10F~l= zb60`!gp{a3*qTACFg8B_oW6#QP(b9agRZd_Vu?zvEKN#Otgi2t>)n4Oy9Zve(}Bri z<_9jct=BNGhs0Af|I>a#*`-2~dr6KDZ?xcZsaHLLwUa;;cXaM9nt}J8y~k@C-=$ew z=kjkFCMT2Rm)fQAmqxkHtS}1MWVkw}8Quw@oj~-1tP9iWa-@bk-H8W@`xe8)7GqpY zsC0#vDi!HsC}k%9{Y}G9%QBfC|o5_ap ztzm(>kN1}e68xSvX_I~L@oT_*+#-*?86^VBcwZd_Lnc{@}A8?sne@eLJU_ zNjZy!8QII&MDpI&AaMJb;&$D=xJ;n)i6YK&Wg#me{Q$!(7a}1s@(zX77H4+q9=PjV zYjS<+ah_;h&yX$A?gRnwo4&{ zcQz!Lpiv@U{$XP|jUTLSXrJkRG3t4HFs1qnbPrPm zk{&vqFKM``d%IuTd)$k`R*~T~(|Z8Cfw9n3F%sf!BzRHdDKgTAbG+}h5pY?x^t>pD zDD>-kuA;sS4rPdG6MqjD$kPlEUWjpuDr(4h+?TBQE<{sgCH7JXabaH*E7#EP^gGIX zqCN8CpZ-4-VvfpVlyy4!A}EXE7-^=6>cmcDx{Hm1*P{zXYRu0d#Cep$!&;U;oO-9L z=z>IqTC>y$ymvDPfbr8Qnd66d^eC3Qdsaf*c8b{p^?CF zmIHz&wuCbbAapw*BJj3?!yc!~PckKF(fXrcmky`BsVEi->7~{7)YGJ0>G_aNuR-}a z7B>b(!#z~;>7e1BF0%S@^EGspRd+MOFVQJ=4nhxYGfy`K*M`yRYU!-Glq_M=e+=9= zT!AzNa|JjcOP3-j%Z1PBHs4)Ee=r^ilRX0fnQ=Y`;fQ&_2>~Vek7_xMXeM+G_jMLj z9X8l#J!_;*o9-tIG)%u1glh>7Yn*WrZ)-XwBeQCt`Z~Z&N#Lc1JqBm9fi1Rd$WT;}B z+pagM^c5&910}9)rSQF@-y4>fX~y9jJwujdetN=i7D%kHQ3Uivlv89HEdVV)CErcI z_mDs?W2G$a^^4>XSqndOfI@2~Db|s2^K2Qc1QAkdt<>f>_-}`F44gJxKjQ+!<-u7j zYORQ1GUiS-_y!}`--qnRMitNqJ%5#yl|G~{r0HQIk`>piM|wMa?y6QdD}J3E5Atb4^N8bJ&|hj*`#O{ zR*|7$kWniaqdmvhj)Ag|PhGf+oS2&)(9pI0XThHG+7VuP^5xqo_tG4T>CwFV%KXtc z+eODiygwK27aC&+B<(wbT|-zIH#h=2!kXfWyA9Jq_bkd<-_AQSN+ZcfoFWk!D*ZO4 zH6WMG*Ey?88YL1jYTqQ?hjxb|aM&);b)!W3mI6X;>^e;95V=0?DVcHQNF{XZKyXr| zpAPh8<0lg`T~^OaT*dWhhAhXE*_*d4usb_kQ1MQJ6DMBSO>{3N9Hoq(as~hZVT!xM zt5{C-)3P*Gl&rM8_OwWl+h$!GxRj|4*31$)J_+-%+1j)HCk9EKLs-1*30~+P*$8Z= zWHBp)G2A|!G}`W;A&B^tW`(cAeYFiUg1Vi=g$AI(Ps^wTK94(Uj{t0febB;6`o#6G zs~NCUW-OM=3eT|X*47Kh^}R_ojb=y)(<}*BBPGUlZ>y`5j(;8X%khA65SAn%BFI-s z(V76vXCd+MPOg@8ol`Sy^&iUWpIg+P{j2YX&1koQjIrz@E4bHTxos)*g3C^3&@dB; z`!mC?6Llh)-aPB&UR2}bjC2c{qD=EiAl8#sxkN6F%$BW9;`Myp>t-rn2h+810rjFH z($CBjI;(GMtBsY%p1kq<1tO0o;mGR-U0NBOJj^Az>Gp2+<`!0HK8eM@bFyJD2dW-g zAGa(T>u$uLUzWUXfD9P6_E+t5G?1-$d@Q6)?;#x~XU;~r@`d=Ho=#{9T$1C)rPaJp z(_y)Cvi*H?O2jB+I4^?4OsAhWPtIqeRYP}XLOT`+pS2oFzt5<-li65?5nTh#I$cu~ z4Pz8dq-C*Gsx~FxdV!~=kN2mZQ60KMwpuOc=bhQ!Z<0`xLWg+jTqVer@U5hCa=6ix z_J}jp~voQ zq=&QqyUGS#{Hf*^og>9qjw*+f_A;%M4n=1_2pbq4YTy}>+mB-@ED5{DdQe%e69TP( zN*drKjOJg<>Z<--R9oYdMO8Bv#|l`+@_Bfj;WI+07`eg;DviFhm=m5j61JWL(cWp7 z5@mq`&khs;KzV&dUn~sYR&o4ET=RSqm^V0!Zb! zgOyS6=)ovZ0D6mkJ86#3vz3}KC9L#7waLu*#U`KMs^8OQZ8MZ9gDJ_bya02#dlMP-Ny|G?-}`K(;Bo3Pg2uZTY+;36S}O z0R(n<{~DFHvfAs%iDnP&#gV>T8Y^aoj~SbnS;Ex#O^wmEUtj5&W{w7)DFHBA7($IE z3NU-<{ecz>LPv(zX1s}vv#FAQF#@svkY4HgULWh0GZcOhRT>6Q-P~+GvIMvW8h%`B zg63xU-w=qlfU4+;b?mzUo@V8wNoa~(!0+JijY()))OdsIFQ)8E@i>ZTAxU&)?30nJ zWwI1#v3!4PApxn|uyy+s=h|mfnsN2vaGS4Rr8auB4a=5eg3c~8$`X@J%_(h8dvqzK zbCdFSon8ZBxeL6a>*hq8W!5EeJq`qj=(1rDV)(`ulhiaNLU|oz_INLu&%!>5_O<{R z!#yJPN;p{EwD_z7sRF?>*FxUcwQV`ncCA3C35w2gwWLY9`#v3xQu>q4<0t z5&q*d=em9871!Tho3;auJQV=W;QhY5{~LQLP(iDgS6%opm!Jwy;pw70hB#73tm=-C zI?Lw34a9?Yf)82`^nhm8Fe(>oTsm9`%8sq%4IW*}o#jOMM3VB@ub#L^GD7{LBKn>* z(V-_LMNF)%j-=t#ed-?{Pzsht54u0|In~1=8@QlL$y>o8hr|%ofMo#{qQ(>zD-AeX zbasBtY6$~%h7TGfa>BxPvkykHXs{;8R!Sf>FD)*nz}D!3!tj>8FZ*C{;8kcC*2bFl zIlQli#&YSJHS#s~IjrpLk_V~lBN@BOPM;<4Z9(Rad|=%%LD%+b>yBGxr_C@&^X6Ut zUT;h_7)7Bv31aI&5et%z-sdVOPodY|O*k6_A-^mx!d*>2KfH}2os3*@8D__Z)+0L( z95w?KbK;vy*qv+-zqFb?tx(C$r&!GjD^gXMPLBxjO zS=Z=8-+f@y)U9t8YQ=m-qV?~r>E&u6lJ2BlWECY-zne25G$)19Qf#i?;<%=adN%-D z|197-#Rorzop*!%l91}+5*8KNSfB(eRG*F-3uFYw<~2LMCu%4NRuUtI+#?oJ+v$z= z0URD}2fGxum~+9Ib&LH`WtXh5J!T7R6qML6m|@CkD1A)Gxb=`rC8@FOBKu!?xT#aA z8Fdj6zHJLH5+|xs54ut(`L#cP_p;5Y>68BPJDtATXf5hKUh8F0q1A)$T<`wE zd$N{t)eA(p2jBL9cp^cP=uS$zf-)MuXhlGOUZNgV``f1XgFwCBf;|HA^^)$#)PncF z%yb3dX{_%o33FN@0P`s&8YZPDiIxWB&$yKlr?>sivU(sZDzaLf&lSn06&@~XeoP4( z+x50U>b8_te>~kBeX*8znnhoA5|EmUCKfVK3(tJ0UT!lrX!M9f>DWaawe3(QO4xXM zZ%8EfmT8Mwfw`mAO@rg1?HO}A7m7A*EB2a(b6j728#evrNWT_080k9MgP(y%kqB<8 z7bDFw|644g3s zXy^{xgv?WYK1e!a6t>)DO<&ytwL2-3 zNv#2Udt?!(A&GxXg8JPE>xSQuBV=--t+}Vc^6gAExnj1X;FQxAqKCN-{b1}M4~R!q zS5wygK&`E^>Ch`4q_>s8TsD#zs{wlC2Onw{x6f7Ey^^-DIbLHf8_)i6J|@O2V`Wi( zf8Lp&oM>hpiQe{0qQK#w?TQL+&SXg_i>ct3dfOCzLD``!#b;GYI{Vnksr+=a%6>w3 zf-`l0P>I9a9ha1T?BMHlN)H(eRnYpbPnQfw*E!EA=KiT6M%eB({n11-p(#L8w5CNo zD;JAakF&l{dxSgEKSWWJw&hey&**uIGCTPWPIHtrPhUjdY}8fb&tjVoxhBYM)4$^6yyE7Ms@M#7t%6b|l!APHTmoU%m6{!&UBMSV!H0!tZRufvCWO%6 zr#f)7F>e^?s4zfg_6m-`(mp%&lWthtnWD9p)c0IpL(x zQTDa)4$44bX3qzxjX>7;ra6To30qz_@)odrA-U+{iA-D*3dMUiE`Q!ebX=qneG<6e&j%tLe++AE!U z>LYKmv=~YCzI^dTz~#qbd%q!01)*Lio_aj&sd@Dttx%PWt=C&lXNCddZ|N<-{c6k?p??!XjH}QKV)yex-gEwbS=!^XJ{XrE@Vew` zonh}=zT6=%4fHy1^OefyLM>nhkLt;GLIj<2PGGW~QQy7+Q5(9W1ZP>f1q5_+LdNyvgUIrn5pH zkE*8V<7+{)QIs}!+TZbQ2n*`o;@XE33ASl@Yg_j38F5_PL*V|R>ot`sgv!+&aR;^^ z?F_q14lUn^UXnaQNzSge$5T6!A#dTLEos{kfZkgPy zRPp`#RK(g$w{Uu`2BXW|KJMef1hHbq_^WW_Jb&TcpI(4XQKDbB3ufz4_K#STIYUb2 zu-4P+4wIG*cXUr~*sgzkKKD_kGq+9);0rb;84m6OY|zzp$PJwuOSaeb82x-tE=6-t ztC_tl50JRHW;Ve1iQ%ivS5Fq< zORJKsWDc1~jx^d6py)sFJg=W%Y`vY6J@`UoH|OWK2tT;BQ{M?U*KZMM+O+#iqm059 z2jlbS&sw@)nVyt_o~Da)upJC0gCCTInP}Z%=`;?cb4{YK;9rl+G+BOVN4ozF_Gs_~ zd%t`{NW_pL;bl=2t1O>emU#Erg-9p7oK^kkn>=9Ibi?1hO9p>@L$9gtj1J8{@2?mN zoHX_|8lBCP8=hNP5X&F_;aW7_5}7!sb90em=%c+JOR)ndopCor-ao=#SkKf{UF-D( z_gU-{uW`Re_IxS}-^3H^P4iMY{kjL1^I@mDfqRizQ?cwrG~XGhFzHo+43M+o(rHv; z6cN|N)gWt!^pw1)%1pF3Hu^Rx#MStcq6XO{@M7NO#Co;O>Q#P#q%YX+g$#jq59I`> zwco#atGA-A_YLxPo&L<`rSiI#>(Olj5iWTzsfG296x!q|<1`wFMN~@4x5o-CHY;|H z6;soZC+FJj3yb*)hO*r?k>V>CwZ7tZjQ`+i&&QiR4;NZvBuhCa3`V)h6mOj{?k6Do zefb3MKOV_+72ns0ZJB5OY!jJ&jVE}22tjcDE5=l6K$zcYz`oY&kRG8@{j0S;T(mXs zEE!2m>5T-Oq(9nE^NAD|hl^o+`$&KZ0Y#YaNXrbPv|W$WT>&!|c7yTldet@cDdO$B zm1g)Z_|e<}bxdTdNJ69>CM2@JZ7elL&>)7WqFi#=waRCSxZ(LOKXQ#>qZdNSy==gy zc5D2F_x^G3GU599jGzZ45^X=_rA$e6ye=Z6S&qVw&D4+J`=30It08P@z3+?Uuyf=w z{3?Pv{4iCG1Z*{Z@JyY4(g~D~FRKlfs;+6GBfRUifZ!Ho*fMX*Lz0T%6!8@M@r6}|8MXH7G2Ha>ZN(Qe>pBxpx@{HQ zpE~5#sl(#6alzo1n{yJw`D(6+)Z$HAovi-)kmDl$R6$b0Ncg-6DdGTS@`<8XVR#zG|RaB}y)cCQgIJNRERd1$I}f1j@dK>i+hQ z$&5_8FOFQY_>Ru3aDfN?3IT^5@w&BV z+0d5|LV0og!fERX#NLT*Ih9T^<{Rj@-=!rl>?SXUM8K}}d=g2+V|^&cgPpt=BOR*L z)^p$iLMkL6mR6*VYJyP1Sg(W>b^GhbuUP2?cFbGFJj)~Zbed55)aV5~7BE&O+ zYHvoh#B{9xK`8;Jlv@I0xxWoG5qwnni6FO~Xef?Gx(^tymQD^Tkc_dIH7LRmGQ%9x ze~ecUk^qEt1)UBDpQu&GCMm`8TaiX1qKW<3r6;j;ZTect0+`OOC{}Aq(VybgWWsi1 z%f+o3U}sozp4nBruHX#^4^s3u?GRf2DTZ6to!~dBeF36uHAr4JFy-Z4i@W~S|NZ(`LLw_7HXi^^S5d88W1-DMRr)?qA&|^xGWX4`jq^aN(&jgJ z6ARweO|hJ|J$+^5{iDL>w(iEw&QBJOzim`^qEyQ$s=``YF~9O;nz7r2y(OYoFFY4O z*h~n1wp58%UEPd@!HG1rr52{8{UN#;CsnzXE0n6fabL=R?Cf-MBwj`Pkgl0@vXG(V zW9q4E*H(m*f6Wy$(t9^3pEDw0@_6aQLx*ZDPjS@Q=FIYA$%Z*CAzGO>&L#Op=lPEc zEL9i3wKfQQPA<~Ay0<~7g_g{@vPIC*9xQ0t zgk7Es=f&Yh*2u# z@gwB@$FqN@8yc9*-jG|jTOAR24HFm_u=BB9-ipIS82V0Bm4Jl0bQx)ggD6@Id=Z z4NqJ=h+fd_&+Mm4Te0Lxa59gTOuj}@A{X5#v{H>A$l*v)QWcLMeXKV1JIsHZZzM`t zpyOO;tM?y@HH56B)Wq`ScXG2Nb92KztaspS_-&xvwmdPOaT_EO_;4X_E6N5g5v_l< zi_j%A>4$?uLc^j`4ZpGRz_4LhljeC@{K2~jx0ziljn2_WWUT2nunHhf|JaC-CIFLk z`h_D~Kl!nlhtKajkwW!SPP>HzG+1!z6)LOh#2)pQemN{ zyi@$SM>LC`7-_0K5F8`+v7~Wp|820B^vkcg^?1}Cosl%ry6S3;a}FO>dcV&+yEWyv zrO(;1Z&%sX71q=j2}dDI%@#QAV{GVds~)})PVwft)Uo+Z)!`n2kT!!N5qkW4D0AHZ zPV)q)egWair+%#~k>BSd(~F0sk|!ngt=X_RYJ{B$bLnd*oBNu0Z)?iL&Ddoq1L{!2 zo1vKf*o=%~?L$i$M@Oq!fC+jI%4vxm-n(Kp9aGI9boVV=cj!R|N($&3froWs<{xV_ zzVFVNzVA;U$hC>^QwBcKoVX~a8Q8s%Jz!V~7FoQ^Admf2-{y}C^*iHxN6xuvF!N)} zELSW5g{M|3)1p=zb}YDQyrQW=_}&=04620`vE z#Ucna6M5WsMz4<$I(d~tJ~>4GP~<3*)VcvHn01Fn+SP7Yk^00GEAB@zx%+;iYMfLW zI=W|waIoLRY?D*zjk7tDy>axZ{cuCx)M!ZOA8kVXN2-%jUVnH8tH?+R-5<*o4I(R~ z11}7&O%a$;n~9mx zG=@s0zs(w%3X8kAex-QQg2<$VO(Tu?o3BAvJQ+U}aa?|R72`{=q(zH~&0sKyogQrp zeMi_efHW!B3?#(8t$D+#HZOSEJt}kUvK^zhQhj;7-i`w&i6qz|Qb-tOe^0<37?6bG zvq_A~INo7tI*k9i==ty&J7-|>3>)e*hsW*;={xXTZCOgMBLzH8EnOJ4zD-o^uY0_P zX+5#yAMMpK%X_AT59w~2994PLT6ZQ8w zE6mXKOCIK1BXk#@T)(-s^JN0a0DK*nz00Qtdgu9*K z7$%ewv@kR#e_HEAmR7;ujpv(T#!Hb_)8)2(Whq$*IGDmJx8p{`COwprj4W&l%x`dr z6W2BK`xA08rFQkDY^2dbAe61o zo0bh$e0$y|Xvd-)X1X5c7N2VekOly7N=xILfiSl7>E0BJZf>y}AyqU!jAi9u6$V*C z;2|tkCLpcw^HT?1R5l&Es6WidIp(OJuT$=f)>6Sk{P$4CLz||%dR~7}r39&M{|?hW z74@s~yTgfVFt`2!MGPM+LlgA*+f=+m^qDOgCxsZNh(Q%@RTbywFiG0J#(ZVGj(W0R@qt79-_8pmbFZyU4ohvhi{& zh{NCM{&7B`3AL7&^y;ji4~=y?}ks7gjRIk<$+Zm9-3xL(pW zl*M4vL|h~=nkw{t1!!QiW{RNH4{WJI)2{SymB^g-#l_b0SkmNpRZr5bliFOdB})g? zRb`V6@XR!{0>E)Rd{lI=*NWIC97;Vi9&bPQ!a|06cVRnnfwBJPy)iqasz5TOr(jz< zy}<73N;(_lXn5ja?|>cf?to#Wz;>X|d@^}|=Qy+9GyubUqLUFNlJ++8K0~00@-dfl z8)oC6rKL2NQIoK6dcOU*Egm+?>|FN(&UVg~Tp-z5wxhwYJ${d8D1u&pt&?ZcU`LdA zv(Ao*raM=KtY=NCtWoe#1V04`ryu+2h86H^r&2+dG8*$mG|KG1AiItGH3vckn*$dvl(zK5AN< zK`rv0k&80Hh!eN|_GP;J^gB5nVr*o#QS1URRM#bl9GcuejWU zaPM~-BfD8W4%t5*&Ye$}Y@vVpPxHj7a|&6N;c=5WDY*{2bj2-NDoYkvQPJ~D|AAWY z#~MXJ{pDiAqb6}S)AN2f0pZu-e$K2pXFhpZG^O+6Z@XMt4(DfVC@qC*WOZ`eh-#bh zb?qukI$&@z5rh&K1R`o_ZpMj{I6HZ@kVuo3NXJcmehqT}yJ7roO&BG3$DUKb!0I;} zl+GXd-*ckKNyI^!qrav#QFP*5cgJ1P#1%Fr5$Fg|rOcL=nE8GviBYqCeB9WaXV526 zhf~wmi>hc$Bxians?ao!inB!F67P6Vq7}h>;@jph=A}SmXs#zjC+Iq-<7^|L-eMT<7#Bg4ffgEiA+)mLHuNA7@tA_*bs(2qq7It*bAPqg1 z$h6HOnsM}$trH^SfChK6uS(qKdt>bzKlv$mC(%Eg~C%gO63 zxE_^vu8?~j$%>R^8q91dr9_=Nz7rKNGk~qIuc?Vbc<3j^9`kiPQ8^sfhAHJ6XZS-6Ze*gH z#uGDT*J3SnK}NQ4D?k)MWKzN^5HrQBMhk4TjFgJb-Mz*T9yti}ItI9!?`p>~3Eenf z%-+_~@p;<}b7U_H_^(|uE`LIOV-B&%6!pDiyCnRE^Aq@{byxkp$}qb;7G0KF4dFnU zdRiT8gVxpZ+6wh=3%~2BC*eNh1?j_2}F%B?K-esQqWRI#2(4@ilmB(_AU8vrv zAs@L2gz&Npc>5fE+!~VZD}Ce=f!8(JtTjjd=y{jN5~@w*5YnI6v<>+qm>G*j`($JZ z&s1YDA@ngJYS(eY*nF`L>nD#u32k~i!$9^W=b^jt?B>z2&%$u?v;rBGXOXa89g&8N zj4U6;DfAk58JpqEAFyE$b{M=BeoUT)I6tv{=Sdv&;qB*;Xe#NfI`RD$pBUW>{W5g73yl^(6jW4H z<os`7aKuel34SxayT^PIde(GL~!n<|nHwhWg#prqbfjI)th$f9u>IhLaFo6|qQAm>^l~nA z>Gd>8i0S&`D_@5YQWZw^f^2C(9w*z2}E@Qc;UlZwC3JyVepw76@Z zOnC_r^=XoV6ug#1el_i^8;_5-msb8A)^ORj=Edb~4u58G?d&?R^=Y<499G%w=dP>L zZ2^#j$xI$#fZ?}Ypkc`DU_7ZvcoxTb9R`3gp*t_9GY3R*pnpQfQ};XaTiaUMU3h&a zDd0ZwRtPB%4Gk?tQxOARq=lJy!}V^9A1bR$Q`(OGR-XRGo>Uw|Zo99{S#HF9IxV#i zy-a^jHpR{Lsgjzq5>tFx%OfT;`GT@bd&@O7?`F8bxuiIDi{&$bc4hojcEIXf@P!_j zgT9v+LbMBr_l9&L9^PAJ#7!st7Q=_rnc=MBOp;AV5oFf& z|NaI+O4j4NlnP#6OK7b7EUJD)0c=^EV#FPmS2XTu1P^g%M^yUc{5B$oU{Qd=cdy`* zNOw2ey`QZK{dq)s*2uoR7L@9Us0Y$qaS>(J#3GuT!%WR~#jzHNR4i2SN1iwlSd6>R zr!_}_{V6>|LMDeb*2v$jo}7OUs6Zc8lu6wv0G49j6S9~<8Y1V-X1WO2Cc^gatb{9Q$V7X-TQNRfqJ zYolRNaj7HK)Z;^61OF)yKHgDQ7Pb2F%PyaK_y=<9+#kAdGN6&6Ke#1?(EljVlx>`0*BDmT4P9TeHT>O6h?veSH4MLbbO7&1VhUoBa}Fij(H31Vc~23 z0UXysthg=4y}o;-dCLm6=zl8`9nBjPR$ThYLL?`l=o6TTC3UZ=ydfcO&UM6*B|iU` zt(4mOsPTj|Sd+lS%oUr%5ysV=Mj(q}|M(Y=9~##R>!{MfpG?!&$E({UOP-heGkD`2 zLicrlmf-MkxbyB8I|HvPalmF>c1Ji*iNrZNNk|)4bnw~u4hO;at*IvN02ffVWE5J- z#DgZY1u_7bH!eLL@0+!CjD~3NrQ^-v)J|O(I#qeRw3r&~zu@*Sys0Q8#hEIW4BRP& z!l`iy%X@Q?OE$APb{?_+h*kf1|dwhk|#V^Rh}7Xc@EdEsA6l@pB%_Px`2Fr zo(zpD`;16xe?rG;C;tLs-@g$cfV6tJTEC;8^^}m$?Q$o3gWboN-PdWejb8hiSzkcF z#m)U{V1($PyrL&S-UO_kokE^%d>0fLMn~kGnVkF+$Mo@mSO7EG0cgJ6>7okHuNBbv zWK|`g^MoU+aN+&cY<`Vd68nkFxA*y@$1O3~QFu$F%yG$Rqduz6Oncg|Q=o6QMiLcQ z?RY(*xU5?8Ur5CeMe@eF^f;V;Au}ye-08a4;A|$$Z|Y9>(&9*?QL@k}z5h{mn6QI{ zh7Fh4p<}`o{?O9caF^vRm$oqY>Y1{gbz}yTMoaIfH|*wjsq8*Xw-$Xk$*}KEJ6mfB zF>-vWHz)XJYySQk(YhN>*Y8cx)8Rmh3JoLXh~Nil2UwFo&7~X$c^*#k2(2qR4eiC4 z+Ln4C?3fUQ6!Yvf`742Yu9~N?*l%_g=(l{?OCJxHA;ch4+3os>Op+-N#E4VXgFTK$ z$};@77HG6nhDR^>JfdKj?l+PTkG-wyy$8X<0yi@37(xa8coUma0?<`9h^c_ThUx-$ zfbU|M|7lRCynK3>57`F>%v8Y0#}w%-6W)0`M@Yz}oDnM`F(n1P`-}@}AcU*-Z=AW- zXD5C}_d-tvH1SGf zyvDL8^z$~ibQo30-N=#G6ih;P=c+Xsf+l+F)5*jis4t=eQW)!YSZ!wL!@Cw3ZgZDx zlqeXiQ52$jtkp&RS9pI&G6c^0A_tcK?JVS%Lk|RK7?8B~3e!jm;Av1OD(ut@6 zuyw)o7g?K7!0#SaT@(os%K@h|=!U7)@5AFz`}{+KP3XsqD`(Yn>h6b4Q91AMYGgE# z6k?$ov+HoqRTqVjS^s_m_~-!rp;#ceBN6w7%yWytj#$W``_Vg>qkB5feVi-e?uRo>>s}_U8&&5Rydy76Ciuks~ zFZH3^8HZp{)}I6eN*hb3c=}@{@GER->R{h|f{7cx${11Vu~I!`PmCNhRBw3n&RfoS zKAjNSZ+0IYPOZV%IqqG5k&Z9?D6)AYk&fS&8+BD}aK2AGp?M(MJlfflM7YvTV)aYU zg6w`vjla=pnD7VjI6D~z_Vh{XoO`gD#iKHgC4;1$DEC>|>1dpdSOJ@wUl^s@XrKSs zHFI)yg~US^f~4a?(t@TrO*R(kc*w)EH<@iv!P;cOEId7)Js(#y@6nKbsjG4DBl)4+ zvEps91^Yg)wN_ON{B0;-b2Ifjy>0}f7egN03ll!q<=)*Li)g^G2rQ z#Au=~?LWH93pX+);z46kR-uJdYhKXzeh?|;pVWU2cTo0JzjjCdip4F8sYJlHD`60$fJSAm9^R-PaOpr?-8R&*40y{yqKeohp zl=^kpWg(TU5678NwNEZ?3VesH1hk5-^1sXE|5RoNy@ZDcP;2Pl8}l505Z|~a-7H&R z_bIxan-7JN!0kPLWS$SMiEbclx`oH=e0jvN6Wjk!@D1srT)H%tWcJ3UQU*__O zl9k+);K}FmmvqsW_yh$KDs_G5es6_`&b77aB2H}C@6tDT3$M?onRo6Y!g0@U|I6=W z`IP&(vP$@Z659wK!xAgmm+?_oY+JrN0(;;5aK;?J-c!`u@SRepIMNeBg!YpCnR4Jt zp2#-F0l&PYOpdl?Z^kT>viS+7Ao0q0t|gl-hAQ<9<;t_UmKoqgTd*gNDy3D{RtbuzNMdj zcmEez#l&0`Mtx%&IIROwLDSW@EDq$zq&~ADrWS)NQ3R5mr&?7SF6XTe4QZLNX+|O~ zt%C>bKGB-EHe;N9gAgazF3k=k7RwNdK6Ho(C#)V+qc)aG(a>5-Mb4DRf#UNn>t)5m z223*xjdC#ch+Z*@Ilv=<_(efYy-h&55{_-CFok^`Q9&AbFQa)Wto2==s( zqzWc~S`eWCj_--1*nfw)?yo~~621UF)iovz1Xnn=cUy$6S%mFE-3Rj?sxW*9s>5r`*)FqzPSn=fOiKb6#ds@)bXY3l@sU7IG+YTg8nlxYS{4E07G;fn@VdHq4b9r4U2Nokn+DgEJ@E z{&p>Y5Fhw)Z^zE{VxaLTEWrTHb(ui_k#$yIW(e_;xe}!x-E`6?qd(hC-qk<-7Czaa zJpLJi_@YV8jG>njb#|?!pyw}Ju9&hof-U@ee7VrD^_Bgi<$5#jq->$aPH666jgH#8 z`uLRXztHnvz?+jBtsw92_vd(Lv1~GNP%gu5nv(Z<>3MZGGmTm$ps$zH7ESIe^WxCP zDlH!^T2CBui~4vJdJlDFRp@U0$;5>dIU#>QdbqWUJsok)gn@H!S47&zy~IQ(yQ~vZN1BI z_qNSX8;m5Bt=&8ox5lC7GXyjV33Dz)c>ofJBbyqO`lt!^Bn^E4wCLwsUfYv9-a*E1 zadHq890;FmhWs=MQOIW?`{?RT!sN&(TzBi!l8Cc--`>mc?+`%AQp_fGp%L*Rk+ciU zM`XIBINjkWw*_0v$Rt0yh)CgEVebx6i0BGWVvSRm9Wc;^TLTIc8iewZH(%8F<%t#l zTf~3Q2}AUqJVm$9-uoT9tvFR&U0FFfE)+aprjk~y&=AhsVwxm1u~)A|UPrd^fID}xwFrst@L=b#3>|Qpd=kt1!UE}tJHt_aNhySe7{KVDs z9a8LZuOvnp6BCiC&HRD$eiJN){}p}H;~1i(STU{v4nYkr5zf&eQHC|2Hq`G&4{;hU zRKdX~S0gRjfS)E%OrPyaf98peG5-?zC@?E!d+b_3c-?iyn8!QZ#v7$xt@q(D?E27w zhy}mty(Qy+o3m1woKihOL4AIJfgA!0%}=HNMW{}iinFPlsuH!W!d=O#GlUlmDi5Jwvv?|uJt40nBf~pe zdZifEx)`gr&4ededuPk19H?QR$I)%c*3SredH^gTXFl)@gjP*|1*T&L=8rf)llA9+ zGO6l|=WC-Iz>u;>4Z}1aJ#Nxa^%SotA1&wfXDs$tz{yXM35=quL z%2qT+wu^ZqKw{0B$T2%jX4pZ#r&v7WSec<}l{HjEgl&Oc7Nhq?9pfwgR~bMv(F&jC zl^A`P_^lK(5~WQ26Rq%QtCB`yoz@*E#T((qHzmDLF263M9kU(78;VQni9RI4%ZtZ&uF?QS)pKEb4v zDCQ}N$@xD_eRFhNQP+2DOl(cq*hypCXk)XnZB5L^Zfv8$G>seEZj#1qjBolp@3-D} z{+qRC?z(56d-mCPpC2su@;z#1U@{Q8hK4@|Vub!g-&?v`4Uuf=-hM(j73>YSCIY{H z6@Iyl_()f->iTZ&HPZ=|!}oIt-#z$nll_iDL;{OSb0=i#%=D0u;c{^e9*+_q`4ju9 zz3UPSpX(YwgL4`BR#0Gi^WoGr8@MHh`l7G*b$G;V^s`71m-)%geXmL|gpU#D|0uE%m59`32=5iftl84jsVehjUFQoYu{*)WVcYDDcnL^a= zI~;F^cX6rk?~5JxZD*%sQKXrb+lgSm@a>`aHC~q~GLOjM8?3t?*ZlsAyh7)_#T>7O zed0_(LziI|B1F}G&8fU?RO5~WmTF!uxLhVkDSr=i%E0c_}`OK3|O=xM#s&4GPvs6ppRiC&`i4UI(0BAK*yl)Xh0n+g=YuR2jbs z%t_5X!*3z>cpE$v?Muhffte%}dYS()$&zjZ5TiU}@gWKmftN>|{ogz^`fHX5RkR%r z;@QFEEbk+eMr>`(X7c0CZi&-?wZYFW6_(zwJFsRVfyG0n)M9KI2}5Zjx7b<*wc>sf zs|pqE(0NJ4d++bwOMMi*_ZQG8#NgE22tOtaqhrI2sk-ni+FU4`b+OE z@BMKSuG;SMM|m5U!Q{;m2_FFH|C&>Ojfi=67=1W7#{wHa#9qj3&9o@WbAS=-r(m1< zzY`XbG`jx@P33mB2cF5fk|O}y(|&aT0KaEv`rbHqD(ahJ_-(p)JF<2!R|s?C*GzXb zOn>L}h!|-?ZsdJ}%G`{=g$jjiFuG)jDciD}aQEddDXQ={G-V@y50v0Y6pzHy4H!1C zz?eaoNncuqk+2f`a*r+STogSKMdZx-ig0g8rw_lcd-TRqz4V&f>GHaTs~LVdaEg;% zl2nQ__|5HHItO$ zQeAU>zhX3_RGZ46izUWk@TTkH2)`sDx_Xqr)uOaHtc?4^BW`^h(7-(qpk)L5XXmu> zMQ?=aq)TnLE(IykqiiVfyE>;gV*OCGnvz|ti*yk5O0IN%8@H{A8Tk1F4~n3FE)M*^ z5tpBJJp~IMGrJA;;nw;4$0_1jhEOt?Abp;Krn2~HQr{ArxE*fk)ABd0v+9ql_c5t) z_X#mZZ&&uu?E#4>Lh5qg2FpF)x-I_k>L>zd94TDHVrjGeG(2XHA{G`D81XbD?i2J- zwk8yQu-Z&xoXUO@2?n9sl&jtkv-+PmRAwU#HN~IBzNzn>j5!cd-;R_MgZ?G=Npo7P(>vs<(W9kM^gz2h(q((=^`OT9 zic>5Er&%ql5hYXqRDEArpj3Sfw>`xQwoEl-{IwfT`pYH5AoUU(wPDlybI51yc=iq-L|4ZK@7(M8@rRo=AO4 zMvK41@Xn5vic~DP{l%L6?h;|2F;&%9J=Wi%wb!4Ia(o`oQp&gd6a6j78a0&AmSBQS zVlH=P!+-qWz$=YdWP!~6*)KAHKsh7x-DTwCAMt$WI}hrYH7es@l*nqATm2AEWK4Vy z(2ipS7z*n^Vd7vQ(N_ixSlXYY4W1M1RCk26;>z)8BPToxpIM>$b@SBRN6n)O@g@** zPds%SevIobw>g%8nV+{CQ%SS4EDh1`&X>m{kgU@MbdYSyKmtGsKsg3?jbgRl=@Jrc zSDTahq-kK$*iQ{aMDmw+)$8f7shfC2+EmvCDFr?mO+ zed6bxAFc22CZwvx&~&A^*tczq@(Dc4&spw4&tK(J%n-0X6i{OAE5GRS za_01Lvp^SUi8LZKHA6V>wGF@WwWtx8y5mu14CA|JyGGSFT?5!R+?*2W4-R=;9ux)o zDzYQ2=N2;%k%x4;l_&o~f@?clR%nOw>Z_JRBm{e+v#f@1!Z*m3hsXT5y+y98?k{hv zxAwST&e8ih;g{3x(GV8>GuRA>JJ@`4VhmABb21(nm0gE9{DT*Poy19(UOs5AEKc98tC>_|}w$ zsaADk9n}JF4{(*+u1UawBrv3g;ZA`;kv_+2)f^lF35(^tSJyi+I(EAZh?_a)Ysa&akRBF2ygx1or+^F|u!4LU| z?)FhYZ0^UvZ0o>Va^vrQsNk#p9BBU8$f$gwU-RR-`F_IA@34Lq`_6UY!*_<;%Mq-^ z$GBAANdAcWmT5V@?Dsam%x;+5f47X&=_5g$YH+*o<#qyG=UQ*+{hsVQhU6*J-y zvGOo8T5mk4VyZ$-FHjk5KURJ;@&=43Ua_wGcEEHgz`~jm)o5NmP6}KJXzwkDZjvpn3HRlu=%0AUL=?!ZtFi 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f6a5f8bc8cc7b83b742dae81f126781c81333567 GIT binary patch literal 491 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10g0sLQvY3H^?81sL*S=k8`3JLHDasB`Q|F>`7zJC4s<;$0&$BrL4dhGC#qlXV4 z*|-0|-hKP`?%lU%@4nr8_U_ufCnz*LC?qT}I5Z$A)ITu9FEH3QAjsa)#m>Q5&(K6i z-$+$MTSZ+A3&?``r` zo6PfSi_Aso6Fy1LTm&r7y?K3#sb<5RwE}_f3bId}a9tj#p1OGXCHpUD3#N&^V(bxH zw3_vT=&_>1{!LuT)~viY_Z*!Yu#x@Ib52{O9a1-eR;re`MwFx^mZVxG7o`Fz1|tJQ zOI-sCT|?s#0}CrtLn}j5Z36=<0|U*QF2X1pa`RI%(<*Umh-mq<9;iVQWJ7R%T1k0g mQ7S`udAVL@UUqSEVnM22eo^}DcQ#T$MGT&!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;y{2;i0l9V|DQg6ws6ss zgrrngcP|}%qeY9CJbU&W2r4S8O)ac<@7a6z-o0<%zU|t*r@XSNqN?V_ix<896Fz_b z{OQxDp1%G)d-u&>xX9MQ>B^O>mo8tnv3FdsaIvzQ=F68a`zK85?&+(nt~D{YDyyhk zyku!&a_Y`qyOUGXom|}?Jb0*UVEpy#SA8Q>7dOv)_wV1id)Ljwd&kaQC1n*$mM#DI z@l$74PfcC@g^QP7zItVC=dgG0zP;OkH}&M2ELad%*bc#Bm)#=FY)wsWq-`U%PFA7$?#`0Q0R@Pi(`nz>9Z3~iZvPV zxGr9FrK6!~MMIZGGuOGz|Nh7CTr_3d^BrH;#?6{-!g-*6Wym^>tE+-muDoQjGI{Sp z)7i7~7Tgs2Y_)CencK1@KWesLPj|lldFL;=>r*OPkPIR($upd2h1j*3E2Ne~KH#2keeH$>6)ttSi+)<}bq<)2*}D@_1aT@rY%6^GPwg zjpv5^iqMNtnx3r{nP@7a`IYQx03a(ng1gWqG!_S`vj;N-yuzbc!D z%q+aeAG-KIvY2Uq__}C{(&cg~*O!l+AMF3anmJi$u@|$L6VTtPC9V-ADTyViR>?)F zK#IZ0z|d0Hz(Uv1IK;rx%FxWp*h1UDz{nC}Q!>*kaclVUR67T#K@wy` saDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0uvp00i_>zopr0MJr7ZU6uP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..992d13e35d1afd326b6bcacc2e195b8676d20ef2 GIT binary patch literal 354 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1SGw${mubWEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a5n0T@!1oe_8TpKz zWPpO~C7!;n?2kEkIV|*|T{eJ(>^xl@Ln>}1|L`|VkVtXUSe-ek<%4A7eLp5Q2hTYS z3v-t2c%&M~5ZG{;Luq2`R9#oM9=BHk1*}geTE%eLG1&5CESVYTISpu%YKdz^NlIc# zs#S7PDv)9@GBC8%HL%b%G!8Maurf8XGBMLOFt9Q(*u?8S8AU^GeoAIqC2kFe13v2m xHAsSN2+mI{DNig)WhgH%*UQYyE>2D?NY%?PN}v7CMhd8i!PC{xWt~$(695)|VwnH{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d5a82d95abf508252a57a23f5836bd64273a44fa GIT binary patch literal 137740 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

RqKrO=JY<1 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�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 + + +]> + +

+ + +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 0000000000000000000000000000000000000000..3fd87fc4f80cdce9061bace900f050d4f93fe7db GIT binary patch literal 100362 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e26cba79999d8592812a66844e976193b49a9c1 GIT binary patch literal 1225 zcmV;)1UCDLP)Z5C;=l305S{dDW-=<)vu=qk1j5p+O>4wed~$hAKno!Mk*^hH*ai+ zQ~eZ_R)blgDFq{fFSfx=XvQHi!nt4mxsx0jeQV49@#Axded+RLWnJ}+^@ga4LrjYU z)jq*0Fk=AVPJ%NE`4OMVyNq8Ol$ZJkw{QJ4F(Ws?-+s8TpfkQ?ks%gtgR-SyMm43( z5UB8&RSVTa%~mQJ>W#u!wDZpck)m1RtM@p#ZdG|a)cgsAgP>9%3N&F&003ml2fUis zlNn#JXfMyMc`E*QV&1_S@z3vymUT-)?FFlMg8*_@K?F4>9iJVh<3NWmkd{Zs{?Gjy zcr)a#`?-a$)4Hmpy?bA%#S`c9v_*@iHHe@m_O3wdmKIPh$gDR5 zF6bQhPawj@D8p>_#0XZZ+%$6NTg=KT$mc=jyzTG#U^|G2FD3xMo)2Ham)hQpp7k@6 zU7$Te`#f=J0kuj>q;BFkkjsLr5owSn2<3-`2>{CV?^W7E&w3f1^&C;{127V4hNLdK zxturPrqf*My9F|>u{D-v`VlOzcjdwWu5Cj$<7c&FV#*ov#3(LHU2wBm@>*)@AU7$A#R~6hHko z_RY>iY9~Mdps|Ks7Y8z}YnsaAdDlfHleqajfovA@!GyO9VHg1X{}1wBR9=gv2;v$+ z`Yw&#dFYw*bZe*zbX7M#W0>JEa%>P&3-vQcnC2|8a$TfUh?>Q?xh%tdeR5#L*}m-y zb^NjCu_5SO+fo}3FKdES5|vGXl|b4_jN&p)3=t_oe_gOI_tJaj`dur_+wEVeV^92o zyw)P(ZOiylKl5$Tx;hc4SU|9*jzFv$cWMG>^fq}-;^u(*@nY)4;oo_$caTBtnSP>J z2oVNid)wH!Aj;>6Wf{Cwyn+o-+OoWp51eH&*^%* z`>nUCyPiVVQ~g%BysS7P93C7106>(K5K#mGz%c*-uy7dgFO4<~Z^PFK(o|4J5CEu; zg@4nB`no4Jlu(ob06ZuG0KXsr;N?r@cLV@9GXVf6dH?`V8UTQ8m({KS`ig+HllbKb z03h}KCx8vvmKl91p`9dUM4`7}u@SyQ(!-pT000hAk|Ki2ZYyUCQnqLc7|?cnp|LBW z9P?&Pl$7mvZBRwXa8TdU^i6)2wj|w9!(hP`!3p&gF*BQEJLmu4q|7iY80@#GFEO{m zOXZ7b5qpR9ptdhW2cpjj-10vMXL%m3jg4m;#n9$X9XhmGao}&pEairgepoWOh?6_q z>*8hmOnv<8q_^ISd$~$?#0KTSX4Li^-m-E)Q{1y4q%{C@ev;{+n3GUM4Cl&OzWyy7`hz$)OLm468JwPlkm8swTt*}u zdVDRiAArP9g6-4&xmP|%Z%ueoLxx3{SjI|jUR2mvDfD;w7Moo|+d$l4c%SE=cYsf& ztm3r))}I-__@Bir=?KjTn9eb7q$D~(PW!&js4pmq$A;V8%)jGB7SEYuvZLr5bqtL{ z007DZZDNTdMYO zfO&ZIr1=JJDRhlewO}6rm^QmJxfs1`enke>RChD_ER!cq$59?DMrTSSC|dyA1yxmv z$Y`&QMjFf05$K@-ej%jo5Rt+Am;64X1yaiLb9@D55n_criNY!WSPT8Q9U2+!0#hIW zXGk8b;?(9AU3PDCV(}RwP|5>?_6-*nG5NW2Y>>hroPDK%U^;YLKQQ ze5K`SrsgIL1>2cvkck(T{gV`A@vL$kf`###TBEt*MDF+KvHF<#9dq)D$%!zc(=Lr$(@X5GW%eNZdc~W#Z`mmW5kk+F0keBMw^`Xus0)(2bFqvW;#xF zEQn?8E3nE1eB@v;$pP_dIo!9b8B^dV(H>^XRouA+^~8pMD?z|#sKnGH#KohU$s0L) zID~)x`P7eXQZv$@GQn}a9I+6BsIvM@n#Bj#Zz_+nnR^q~-8MEhKPLk1u&?0bVf!b! z0dVceo-oO-r=enld-bqj+Y!Hul2{Mt)?!uqsbnU{gwP!7=FP@m8eR8&nFf+4pjK5;Mg+XwI z>l`6vk2QTK`(d5He|Dtv>lf;~uq7L67y290Sd)ja#=nJ#6P_Q30{MZ+p77P@gw2om z#c&I8aKOyGw+#d}CYrx$4Gy)jaB8a#C@Lof4>6O22Q4%8;md8o2@yX|@LNN~hr-U?mPH+o%L{kWa3#RDXB215 zP5v~RR?LqpOJmVZ{CUKK@qbH8fikzS?!LWctZjc48=`_8q3Z~T@qtNpRM|PV!{8KZ z{Ud`x!xY<4q0tcjv#X@3t=ikl0Phwh>8WUbgVVMRC|gR?>CHqfG#xosvEiGxh%XcS z)x_85hv)PYH22ij&!SI+@vc{!O45 zd$cTD6s~ath4!Z12h%*uixl1>`m@WfM{qabOFy>fxd9{$Xg-{=@BaF-`LBQ6qcT+q#l+R@2KyygKkP(p3Yt66t2)pYq`-uh!g=jAfmiHChG8R z1u~0Q>${PcnDiJXCS9daO(8)B6CuPLd9zC7i(vcnae)HLhj-D;+K12FTtI=yZ1v&p zSyE#`Y|VTNxKZ)UrCV0y4%j3LR9o!#$YsNV4ThmH1lDF2`@IFb=-C@37-^w9$_XP@ zm}mj{@vf%H$4`)p!mR2p1gpcNDvpdnv2qi75WZdPwAGUWCQdmVrGH9j1P@{BXo(L( z3t^@ZZ~Y*}sGqb9Ysl7EHu`A1cK<2$Q_?u)7?MjR$Fb-j z250ymoDSMc5}Axk!?iN1#4O>gchTL;aXqJ;K>rs~?-+Pb=*FZXnr zM386~Xv$e@#j&|Xlm172K&Zyo)OBxV>XUL`b+(^g4z!{X6l88{n*x6)9EO+r=L5xK z`(g(o{mWCklWpr^4^K6>QdLe|cN0yZ<29_pcVR9w6B+b~I?R$=#DPY#+r zINlQ&P1=X8Tp#ep3)^I@41hvh_-KmneQ)4WjwqC1Mt**AWr+xe2fQw=$1vk$(x^l8 zf;hKS=t<-GL3HGqcHK|8OP>~k%vezR_Gxq&&f}DFO5il@_Y)uy!Wh!MV|<(dqeD!j z*r;&Ar-Jd{35WPiK!5)987rEOU-kSZ{HsnwFQgx;@ z7na!G_$lqdO^()%So72MWK7Fc%=eJ;JTg*LMhRDuMI4RW}B+U*J zNvI-;unbSL$Yf47>0pM;M>5T1IerLK=cRhih{y73t*qOjgBxGjJ?9MF?&gejPCaH_ zI+`%C-98XLqk<4M@CB7TmR9)%PDm^a`^{>&R(X)srWHep`csy(;T$_MsS(1O<}2H* zv)7et%%>4NV+aTj0?CUcvNhCL{YZvSkKU>I0pzs_KSQ5rG{!0sOUqk@Z|YK3v!m_g zB92#vmnt`W7=12+Ucv4>L%lwr%440xWB@!1?_J_Mhuma&U>-ys@3)r&u~xz2yqv=j ze$$~5Wd||#hHTPZHTJkakMTc2#QJ1AsQYbjVU=xt>!j5Po-Q0M{brLnG(O7~++v|a zd-m5qWAgK>#mHh)V!LllAB4O6^s@d|Tk9i%*oxc{gpc%Ew3zScYPb?zkgDb{xSp;P zwdvDf255hm`s#VO$m)jfq8SlR$Mm$aA=6ks&)tk#RR6)v{LEs;LG8^ zl^Idwkuuz)qnEmt;ztt! zeB_#(lrERG5NdJP?D0V}#OC>ha5~sh#i=?5*ofGM5%iMb+?sTSw0Bfs(X|N)Wk)7M>pojzmb0Tm(21 zh-EOo)TuF;GRV2B6L#1!gghEpx~a4atx z`0pb&(QCk=TZZAuK@~V=*GrlVYf+qxq+;si z<-pn$O^qzeJ*$pl$8{g1+Bb?XfCciSqMcGeqXkM7-sh!n^1Ye^aZsP2n-wYnlfj&-wz-$yYK;)lQyCJz25MMpr7Ju3%Lsv0zIYmFUYj0hH?dRNI| zq1KZa)oTa`@biQDqDKsfxeV#U44|zAa58t~E z-KV68QD7j^kiq>0h~wP8F2Vi#1F!fCA7^&=?#9_k9@n7Bpot)NYZ$@)#8ho0I8`^B99&A@m4>xx$^SeU?cI*viD2^f}C8DKd z(Id!*(&%e73U(8$@H2DHvtdMxU9F+l39$-9I6h zo*s2b=kFb0i49N&c%9;9JR&0rV#_Lx&leIPP9=c)y`Bl}w87yPknK@4o4mLjGyqf%khd`0B_;xwc z2sYj!U%rj|j*>#gw0QtH+`PyM#a-(VQ59R5(5yUQ{M(B(dyuAi z>K9uVpOmV;$a&^^iWg4`4v`)pklsHvSn9S&`pc;4PphtuJ0Y~o+VS7a+92YDf}I8c zZ`|(c`HBbE;nhtjMxXdVpP5F7vQ9W#WQ3Fg=Vn~bJqo$Wgv2EXK4P#EH7pKKyZ@2w+p_V+~N}#U8o)R_E{H0BgfV{@OAp}-dlgu`~Equ$4}_p)moxD zXw*Pw_Rr1&l0=X(F7vOt(3ZbYcK6a?(%TB70xl%sfT>a6v7=^-C)da-4-+OxAqXA)9lDU}e!gd)<^%ypXwO$i1HFnL(awL%Etfkgnj zQ&o=7OS>B0R|_Y%ic25_8ok8tmTE9UHMV*AI7d%dDAVTHBb2_5 zx<$G|qn_K?6txLN&eQ=+8tp3cBXssXVr z4%<~IpXpH<xpPEh zki95Mk>lr8iGcTGz=Q6b&APQ5;z-JccNnkgBFUq9h2VkxIJ18rVrn=q2dQb>bN6&z zCXAcQsp0x;i}m0r<3uW3B7{&Cru~sHf&-Fwpd#bVR76m{z6lbP-?Lmni70zPIB)G! zylsUJ2znsZH;zJY=E+R{^32WN`+U@}v;;QLPe8n%a3J5c{~8hxrn}{`Ke^*ihxS-J zjV;sKcis!xo^4s52icWJFWO2$hu%hZY%rgraO>#&8V~~aHUZ`{$Sx!LyuC&9d%wIv znOj~IF2mn4_J%rd+sT{{f@al@IAWPNaafrhU?8*WSMUyugpB({OOibFX0*y$>h|=1 z8)i>dj2Dhfth$z-KFUAWZhjG&5$=mM-!u^;tXpCzeUB>PY~D!srfPEExqzPmLeq~# zd^eY06j##YP0|8*LzE%z0Xz%N1$l=g$XDfE3+!819tf(pcPe0klhPriMwT1e;Wo@h z1^%8c-S21mp{mjFiUbTtaL$AlT$-X3N*Af+CygE+5dUJ=(o=Sj`ea#xBSA+fb`*1D zbM3!9DAr-{Xpf=o#&RG+4S;o)k4%Fps}P#z8k>6*Jui8~cTICy0uHa)N3Q zk^@C^3PTBo2X(D|j9PG$z#!MTz?FL-k9YSa!)stqx z4#C?jMzq2!ME!dMpfw%rB9+n$MTd&06xQeSnA!(9JM!{3`IU=%@gya#eU?IZK*QL- zH2h`y0$rG)f8!AO+hxeAL3YzsVs$tS_(nqYs5+Tz6 z8&D5h6FO{CO^!A_8u|J2rlWr8tk7|F=vXNU%Dkf4@f3W)#3sQdi6}*t{!cgh*Q7*i zx?d$uG1{Nqx2(cJ7E&|ZzEb2h?33|5Kf1pq^mB86QDn^!Z-C2*Z?j*CD3KU1G^EhA z>e$xd%f3Nlq1w1tD|ZhI3o`?fk{0~u;(&ydG$Zu?rucu2`QJ_cSDODO_Fek(kV9U` z^_E_tlHz(DKs`zrFw0`R)nq;~4!=RlPGf4xitS#hRjD6@5|PAiIstPfE5EEklee=^ zcGuKc&oRTz5pZKQMRch%d6{+iHtCl0=>6%Al-#c{S`h(K`Z;TO5 zoqYU>6Two19Y*T5WmWBBqCo)};^XQN9!jm;tffA+ZE!GRrQjT|tkkcs=9)B1`;1>b+- zcR$-1T`E~n=a*9AeJu4K-b_G35?+k8P(4AOG9rY z4(pFryL^#wp(JQsaTiX+rAjIi^X}}d-liorw7})U^8q%1Br zb>QC$)03w@>l^SISc&*}^kk1fD>XK!JvfQWLJyI`PHntK993nWHe9+@Jq7*~)#jOm zGKHi*_#{B16|n< zs@fv@fKNrdBx1xR5&XmeN?PJ-KOD5U=4n50tT+{eKAU)%9&k|<(SqNi1K`QTl_bRZ z7^vp2qqbX&1lenA#ZLhl9NBw82#$x7ypA7_d{f+0=^qQr&d{SlV)bX)3*JSaTXNNo zms791CzO4KPv4o0T8~)|zd5F5aTs!_$TVCAjsq};Z~Dp~E^>4}h=G<>zJ{_8sVKyXq7*T);&xaQnrCijwEQ&Ql>-pW1x z)3`J#COI5*^u$iB^;Kd%iC*Z}+ zTqb1;$C-m~41XmlqoMSL-Z7w~vbJ8LGyBA%Mu`*X6Nmg*`L_&*)B&z-Ol*IstOq$ zMIm3j`EX(lTVwKmn=_q|I!|mN%kAL{yzSO%!l{7@q7hW)9+YnAaGcs8=D0}a02u9C zl;hZLwwbQ=OO`|AW1wL11-em3X+Z=?x$au4I3osix;@iQTWQqu)B&Zj+u)9J-7GLz z7&@h{lGX6tSh_qk7~^?WGvHh8@IBW_qpT7n@s^bRnBMUoe;sZa;H3h43>Wrn%RTf z$X*DD)-wquM;gK-PrytB!8)?j@+Y zj!*t$db?0PdQwSS>2+UiGmU0_f{G>3D7a?Uw?pkl$BE%L=uNk%MW1%1$D33GqFC5` zIzc5uvV2v96?wpsQJL#p_n0IBtXhp$B?5u6acd*+Mk*QynHj48ksc`1B&_Zs;Gv8w z(txOarv`zCpMan`A zvgv%?mslB|bwlxZw1kbLohpaW)&W41RpC6)sBLoWhM3brbUH;;)eMMiV8 zHkU<4p}=%&0Y%e_%c`HcAi^sv(P3{%ey2nKmZKIAz3WaxfLuP4+q5gjeocTbvxU{t z>__>p0udCj38Rr_kKQ5|QG#^7T`gl*C29eSN%+*sXx~1S$Kh?loQE{a z_nqhsOwP6eB%H>WFCs-dP4E4XffSnTI<=H@-4(0Td zO>IXB-Aw!X7|+gEoZx|{`cxTHt~bSpW>nRbi-LO8x)7Yk&0LiXzt^2@ZIj72G*w9& zARUo}$kP{0ay{R-7NJ43bh#T2ztCpJBDG+m#yYKfwqd-$c7f?IN#qiM1szI58P4)j z{H|*CCn2SCl`Vr@FVH-l*Gb;{IS4VS#Yub08V7YG>Aq|2EK|)lk@ft=$<>NybP)PQ zOhtu%J8{B{$;Bgt$0FN@M^+Y83`B$4Jw0}{+J_F9gCa~P{!CEf=5BYDVp&d;M8yc2j%<@I@ zp1~Ap}&W&?1uE} z*=JUX^zLvpPw3(uT$U{ zD>HMkNC+vKsF1%N&-!<=pt$diA_Z2GLn$fZB+m1J8khNuZ~9S^Y^1Jr-rK_3H4DA# zXNR4him zioBa0V&JnAYonaHBbh0Fk}WHJV7$u@erwSlCa*X-O=j8{8fRuvc1qY6b2>?f(6%q& z{s+omh#9#_{5K#VOD1fSNV0(Lx~2KExVL^oJXKdb46h71|NB#6yM?z;SC169Xgfd8*= zSYf$Sm2RhxaQ(u=ly23v1I<_^NO$&kPMc?Lc>q;L#xwgQ+J|)lG5;1Tml4RZwzKhv z>t{+!%rL7#6~oN%XMU@#o}*}z<0WgTUzxq>v$ zwwFu9^cd$0J%xpZJ|uUO*Yr6HCe5t22Bz?1Gx5yy+R(A`!;I_~S}7%dZugtX<+#pD zL;%x=_`T?ghGk_-a)1z=j3xfhb_v9Cb@tx@#7}v@cO&rBAffEZ@W}YucN2d5{brYGm*c6Z=55T%uNRx0!xR4 zXv)p|$O@-+c2p=G3|V{K$`Ggq%N?M_cIdt@U<@51Ov>;q^tHFYp1~UqL?o+2A-CCD z5XZ%7xr^>wFVha(Et$Nh#|MY9+d8|#pRZwyQQxm?;uRJtF4Ix zp?6N(BxeTu_mnZh|Kez*TSx z>imP2da`|?Dm@WJOD%+9CL5xH0D&EWlgt;`dzfHVa>+SyB*)!>9W^jUVbZA@;DF*% z9ww#bTKER-Q@@<3z>!~OEP5u+2^l=SSqZeM1o<9Dc&T{>ighqTZ}Fq&elak7Af|_e z>RWGQc_lSRGnM6ebRH#TDDt|2i>QYTo%|Lo?B-rQX2OjKHWIlw5-CL|?UkMuaD4#Tq^fP$7?s-7 zw1j=^!M+$E^jFVM*m)D2>wMMVs~*yzkx3Qq0#{b~0rc|-Ketd3CeoRMl8JF=oY9Q9 zca#f=gPe$=gFJ{yL2*6txMHA)sz49LV@II7nZka8EO=lXMvWFK$I>@ARRy9PqmIF= z@XJd=fsuR6?YUeW;XK%zAWdv47)arqGp!xlU!NcN7b@C|WzOARu7%X_1q|wTRYR87 zN(|Ne4xj%0N0e^gD*h%4P|D+02xdh{J)s_M}QGuyDP)z=-*!VPZ<61FlevcmJcSgoqX1+h~h`f zlzCvCqaV3ovyaCd2Rr@vq{@juXNDZT%y*B9d+k<<1%y?OiTS}269c7eShv)|R*gh% zZ7B`hpOz~?%NSaVd%fWN5|Q($?Zbw0-Pxl3m}J*`0_==u*=8&`k>2_{QCV3pg`wVo z^k4h|N)FT4Wa8(4(|u!AZ2Re3qs`p*J|$uFk3sr2IvnOFq;G1VJMtTGM3SH|aosh@ z1vn)ckQd?<5ql5{Dey>CX;HgkROw}#gA7O#gmMk@zX{U+=>Dg;-C^#W`Jv{FsLRWK zuHPErfJX`}xqZ^F$wU$AH?$86E$s>yM3xp;H)9hN*CJJcTgTfd*c6HCHw?YTCX4N@TZsY(^N_Dc z3;WcqS#8kE{X(gG+nwK2_`6HRXZs2*g$5}_mph0M5tt$);cGB%!_O*m*yA$2>In`> z*AQ+C$4IlhJ3+WUmPXIQ=>-1f`bq)I*X>!G+gtwg&yJ#T>=~k*$c;avxTxL2y?I9q zL42(p!_I#SVt38db=U>nP8a*t1M~^WXtj*Kh*7ShSjP(;=f_)Qt{#oJoL}Bzho@PV zE6=V@ex0n}ZtZ)CPrbrq@DCnqz2B>hlGB#tW3t)rBRCF5uAuktL&ByE-yg=26ETAZ zjMQ-GKecHZ+rorlP-C|vhx8-^`t_WiPbwE{UiDO)g-q$joyT@p=M?_JUzcDyPaL0L}=$*2httHmX`nai#H>Nw3 zK*KOuxHv#VT9U=|X^&WwN1t;Y`EPy*;vf#IwRy}~umniVRE+p4G{Clm^Q+RnPTD>l zS|L8MgtEq@o^XV9=IKD-Sj>4U`4T!*nQ_62i61<+>B67yzk>MO>n+r4*8c-B*A5BO z$pEk6H;$E$uMCasHO*8)x;i!tn?e1#)nczrw1xsW`I6R0orm=E&tF?ni-(cQ?6U z9BDE$qsz|Z-v+VliFD0iGoSf z@0%eN`%^4|=j{LV7=0jH`~9j)Jt%$KMC&~AO;+fuvQhr0AlX3``g+g*OVryr-Ey}} z20Uwqp!A}lB8L!|cnMvDDoM>D5^EB>kXJV6D;JMOI?oGj`udpquqmCZ+}C{ z^XY)}Q-)uII~``AO@ba9Sr7`AzngMNa1^Me0b#HjKf`TnXoSXmTNNP_xK`ERwAIXM zS$#F>xvPC`CQ6^~q4W93f3~hEg`rQ-vpqF0iYf|S=!j!sUP41tUTjoEZSGXiE1xvZ zM$;ZK`Xyl%uxMyeF$Wk5Z6doweARKt4N?2#-M0h>Q-49bIQw+423{t(MU_%wT@vB@ zYD-Q-BiAAAppGf`zhG(ecD7zL4YW#vNXeOLaB&D?&gB=UF|H-#1T{2PS=X3!eLxAx zklssx)9*mk@ZX`R2M5?#Lu4r-NZdsJ)DoHXn~ zEVblEoB+M(L9$=*f1d8rmn3%Ls}Rz3zB2;DBFZKE$=WJWPpb##RIk}>oNBp%>1t?q zO_+YZa^QY$0Ae5@3>vTl_I3P!++BU{D*ST(hY!|sv%j~O;SShp!2G|b`foP>E06E1 zf6vyg=S4yY8MM3p`LtRl$MCVewKTN5Aa=vz>#E7wY6~awLtg6{MzEfh(65%3m|`P$ zxpCtJ(pR73>Tq$h^L{O3o9tfSuRhNAdtRd;_XjRSh}-zg+-K59LQ9R&#bV}+62!g? z9(@yWfRF8-u4l`iyN9)R!*9K*!prVzwrjFG*IV1WXB`1m z-l4Gv**Jt!0}@i2LkHG>TrPiLPz^2Gip`?O&)!s=|9LPu=SNG8nmwGj*bsMz68-T= z5QgFX^yF?1+MzbaJTwT++6^YV6fxjEo2$TI9bfC>r%x*bkIewXc3?^7_>JZ*hhpD?n1$qHKEKxLxDI}GJuz9x7zngz)rHz ze2L{BT0a42s;rQu)^lB!E=kMa>bKoJ!wHI~x_hB=i34W$c3{9q&`?Gc=} z6<2n z74C+PQf`U7I}!p2P=n`y3d zjSUV&661phsu&7&@1$2)sF{Di=GS%Vt>yX3_}k=M0PHr1@3aijaz9BZ6h$NMBC;Q@ z@U||* z@HQf9ekJQs;t_Oed4dN(0)6tT4=&C4Dxpcxj z)K=pd2hFiII12ZY4pLDrh+lEpiZhDN-bGw@mWhiAdgll)49h5Vlzorer6FHCZHMf*)yK2SUSzr=p`!zlQw^&o%eY`76u_)GyOxwM%Iv`Qk!}X79EzV z2~TpH{+^no(t|fmkP55|VE_&k?=#zn^Vm9sgULG;D(U+O=6e*?Cx1@qx;{EceG?$Z z2?V?>Oz74I8FlpQ$UpUqftg&DNCAdth9lO3n)B5r!@_vD+K>=qkC=aH%{Mta z6>y#@$h5zc4p%C`@HmA}?ezzdC~B){j~U}3FgQ>QnWBi`oZw}AAQm4A+ugkNbuD)u z;Dms^)jBjlNCiO%kukDXDJU#a2 zV(ERerIB=8t~J2brZ!4;ARShX;q{G(FD8n-+y}OZYkr93m82*HL`=(XL=So0_Ib-4 zW>uDEkB_-oS`1oxn$siB7$U4exCR*pgMKsAW(S5~OlI8>Lv82&%(0vjc9%FEBP+mU z$1QMy3{DdNtOk35L{O+{^Vv1w@zcXF2jf?8r+PvY31j)BEP-JNz9C-EHy=m1t4#fa z-96|y_cKKp`j7r`h`{)|1J-sLf*(jP@WxGvD4&9f^~Vq~Cg`W5;dOTJ(qr!G%F3~T*T>EIWxtUVnMC&yXVUPlUG?clp!kgQ zR%h~9Abwwx86%!hl_(qnoT;pP)aOSZDN=X!EreYE_~?I20l^dFa^tK-Pzo}ea&gI} z+&JfV3i>U;(L?M-@GjCB>_S&6P5|<0D6RI!X2<>c5PXwoEx-$jE00zl+cB0~R*G)D ze8Q0g9gNZPxrRo#S&HlJ3KDTh;#fguS=jlDq&^6duNL^Bk$R zQ@CW32>pz;Y5`}=kQjCoM`^J2K=_kdIfc55GZ|ziD(_m5A#y%xjHd7(-v^M0h{%8Z zA8t@kQTM-mApReUQDG60q|i&D|H!Z~YGhQ@|B;S1O;^3c)3&a)w!76fytDFM@@vb3 zuaa)YKASH3HKHiu*ZiDcAHQWz>dgA4=Jpjov#*`NHTq~yN( zGjFqfRl6{sV@U7WsvG%{?{V2NC)IKCa?SD5MfUH}($mZ%kuutUJWg+(xG9sXtJ2eM z3*-hv7C3IyBjy;&X}_FM?Pf+GYBBU;8tGmk1S{aaY*F0rJ`)?}n$eYDD>}Tz-=*S<-KZbv zHv+a;DdD?lifBsQI~gy8j|}nRRt0U%hKe;&)b39Zua%aRsvo^Z8~jf$lfv`Ny1ClA zw_bb&+;?u?Se5h-BfDR2N0hJsC}{xo$& z044WO1DR~COl~8tW@roc@^cnk9z@PRd*oJiQ4G)a2;NQI?j)6mA>g6b3$`JHJtXJ$ z=9Myx!a)UMz}(jCRED8IZlS#{fx7xO`sBokiG8^#vn7?i!fgcvzu!Sd3Zm?MUBX$kjH2v-1Jux+Gdnb7YKH5(Nx-{PPR+o~I> z$Rbmda(+?P*tf5tw6EzhC-+ct#^!3;LC}vT)Bfw5)l?w~G@Ux_-kiUn6Np5D?qWxYv`DJ?w2L9VPe3eV*J4wVnhh+R?0|8UtQ$)+dS+|u5HPW zkH)s2m^{|_(XJ2%)frz7z7_%@-zSSo{x`7CbE0~QMW|w?eTvBM>Zbv1O4Hd2R;zD9 z=*I6TWv$MX5KZ#9WLA14q-bKJLEruX9!jOMv;Y^UN9TJ!0YTw}5Tp<$TuZp*b`kOr zRI9LM!dl;Y+Y3eoQ9**w+=7HzHtTvU1rItJ3*)69l5{e~4J#ctP`G(oSr1J9@y7#4$&j49)NUUM_|xcie2uof(cBV;bvSX#-Pzc3 z725P}12Ft;*wA}e7?7mL=l%ftZjcTAuXg=AvBzKgnvWql-Lwu(Rn*it5C>md`$H(X zf`$&HwA?1ZhGYh|Lmkbjvs!elV+ugDy2})!rJ14z-GNQP z?sdGOtYYn^9liE>*4%t|#0mA+l8Ww2ZL|A2+eJZQ08@y)dcHl9EgeMWXEE|b-AB**HyM3oy&g=WN8e}+Sk_y&%YTcgG&VgKumghUt4 zmoY^k$jW&p;kV3P5UzI_g`z3?zE$p~4h28zX6%f}yJ_mf^39Sh_iHz_2;$*_>*gR8 zWWc>vQNhdNSR0CWeTuHc%`rHrpv>d8MP>T-bEJ<)E}X&{V!3r8KS{T11VUgQUBT#y z82QJpUGwkCf@#|ynS0x%oZQZ;7b@=~kqhi{2(S3a-?klC$}HU^>22Orz6%T#+C$FC z+m!*>QHnzZfjKqGmYl6kP<9*wz*8~c3V%KZKxR0Khv$!g07E;dpY|vgAzgN}C+~Wu zprT#Och-p5ja;O>zgke@f&7Mz!70ZHtqOG*;uFS`MQMd`+|o_6`|lbx^$rFa$G2#I zVp$CG0}iDQSc3MD0wN2 zxH`jye->?TrZFcrc%>?Z2fF!&g`1B%kZ(BFJOfO_tXZA7-=U-!Sy|D&$2jEW-&vn9AY1Rb0}(BLw-y9ReB!F_Od z*Wke=XmEFT2(AGJ*TMaf-S_tF*|V>IRCQP1ue!Rbx=;02w{BZ{!p~(EFzb|W6Vu>k zd6z1?DaK~0`b2(g^vCn4*G&&(hf-Oea=YNc)pRAyVH96u`kZX1sT3kK-WFN6z@qoL zi_)9$>z5gPp#O9`XCzI!tatn_ZE8g*mWJ;q@I;$W?CbJ|Gi`Y%+5;Gq$)jIfn+TdZ zTis61VU!GXsTzdC)7Cgiiu%yAZMQO65+s$_QcuXTgk+zVw;C<$l4Wi$r$-!1(+TL@ zUXCiU>w$@3hXqL5^z?zV^5w$-J+J6=p%kN_`C!J1)a&c~7sL?-A) z%Bp1LnFTqpL{RlxBH8(A(t@H&m#;m&_x#lqW z6jDhW#P+e3qI-kmqB{;a%r}uk=_J`y8DUy7tYfe@RI1)DZho*pIx@(QnyYtqAy5B3 z5rm#%J?xzxjJy%nEp{m#C62C%I5EDu=*l z+UtA+D|}SbZnf@$Q}0$aoZS4IE5hgC3j3&ainm?s<56n>7kY6Mbsb?CR*8!n(z?B3xPy3>ao3K6+S0Rd;>-W?_J@# zFE49e7t#}m9yPFztu=Dc6b2e(hJE)TSwl)W&`?XKSQie&vPDpDahp^44vygt$@zatiLv>_0t>ASt!&42(ZcC~45w{Di&mPZn12=d% zk3vBC?FooLoR}^qC%66PVQN>fXi|`X4=JC=v-xG06PXZ*FONn5bPlnLG&9DqqGS`W;5h8THw~^Ez8t$ za%jrVa8(u^O<`#FeD)TQ@BZ|tHlVnk*y%F8k2eV3#*m0M4a<338J1uaQ)8BmguZkL z*U(OjX`f6yc^`Nl-5^+O9H~Z&MKqMVA^V&$iYdp^<_QHuz;yon{e3)c=1Rk44Mar= zPRcY;|2&HCj>XljY)%BIyI!8C_myP-9F0)#-a)8;zQ+Fx_?MuJ|KF0mr~y&AU9$5! ze3@@7mf+9d9iZRs{OIUY)#^;QUn-0JILAMZ=}08#e1KYVzl@im0BZ@=E-VyjysvSK zd#=OgkEJf9E16rqc|63MyuBS-A4Jv>5$GZz$+*bU&08fCmye#URJG?|Uv5VkT}zpm zdQi)}*vUJH82XMkmZJHY3|_C_v|u|yK3C(Ls~2~wbf~GLSCh{SnuO^JK`gDOIfSc! zXYaNR50UbD&`E6pTD@n%W}p zbPFdM>+I6tF?@1RZG30nOg*KWEzZ{`9pk}+h0!2-!@-WGxswW}&=v2Cahn&TrcRP_ zzF#6EQL`%#BQ4$Z^N;Uj&HY^Jp$ zWr{V2?q2_#-rr$J`ss0|y($a1ZA~*}%!4hdatkGN!bN6hCjqP}i9Ft^3@@j@B43a9 zR~w1ZPdPQohn^p$1^r|>*8tjjTC=#dqX(=kg&W9EA~3^r_!wiME2ROv`R&oaJ<>(9 zh4P0;;i+U}YnBZ4xH;$X%=dLyoD^u^aJmi?r`cq7Bx;%di*q#_FO5i{I6zrE?k6!_ z@~Be3*rEzeIiEUQk?ai_TxoLGa@*p3aUePR*7+M{AmWC}xAG4Xv-L4)-@52boa3x7 zXr2=S{!S}2hItQ{bgSOlxLaE8jX&=@Ay3rqeB%#~3#6)Y$d8%4n#YmV<}ze7Y(Hb= zV$d#va~xMHuJdrASN&4rmk8K=mPO-0gN)2s;)74PcqZLZ*(Ud*mUd=x4s8;kA@mHl zJK~Y4_RFl7THJN5v}!`u)F(Z1=PsL0S$yW>Mm7zdLIF4Q9wd;7Ah7j!adWg`f`x|# zQ*9wr^Ja|pkM~X#R}W4D5U8++0kCV6`y~G98wWvP%7uX+y2UzxIhd!Q>F?^Dt{aK( zK*17U*UkOIag0vM_1F7Oy-KRyY+DL!thg5pvdb%|VbAl&pua`Dg~Gt4$-mWe5acz; z7l_GKPp0m#$2CQgP>~E~>zKBtW6_fOjpj zo}>c?eZp2Q71nL>6`bSfsMz@MsA(>=bF!wCNP%7fbAn`eE0K?NfpfJ%t1X-YudIt^ z&iQCs2UfLUJZ@_OoAD`g9C0FQm5xyHK*_D1EVI(^{)y$;z2dzq%a?>14Fd+*lQ- zq~pzzH|_mt>gdxYbMC6lyt&3g9OzFn7-RJ7KIKIMo(Q2Nfg$9^Bquwq_swp~M2s^un%6wwiPf+vgs!+F7qnjE*nxv^}g262z#G z;XTc7KRJe<@sPXu%JI)|GSoYxl)~WRR9jnV)=%35t8j! z)c-i$&7a^{wl_Cxu?40}@REs1iPl?TcoTV>A?;lW6Nb2$D>B~F z3-b%CezOm13K=u;0-+Hgp?e1txJz^Rxd*qrwgN_>SIs!V4fyC%bA2 zXK^to73Hla5O?zDLHl=r@WwZG5N0kr6f$>Id6b+u9i3q3!vrVmrfUYQMG^=oiMqLv z*KIiL`{{)#9|Xe*J;+e8r^bksxWrA8kV(xVsYYN}&4o5}FK1CO*x--l@uYKakMm3+%H?cGw`^Ub z7f}?TlRvx9^(m)bSe>$5T+^A9t730|3PwzkK-aoha=niWVq-ltXh&*T;=lS9S)b7b z6vT~j-!h|T=nzBL^z)*a@NS@R1l^p9iYv#reyN%gj(yQ9J8}z#Nl3RvILA*1yukpv`ODs_i`L2Q$9dKP^)|w z$ZHHCJk2XurlWilTl$>84Mqu`sWz+cMihtvT zN@@9JmiBzU%3ogLLEsW~^tq$`?PA8pxp&diFe2>7DJnL`y{Rc19$z6_EKhZ+N4Ja9 z+49vjdQqxJh0iK0?#kcg$rJfY>90Uqle-Wda%1!TL#GD#3f2et?_u^19^!Vtes3pG zxL6#gRe!fCA$511C`ey^(EuU7GiM$;mUg^+_Q{0O)ehoEdA8eo!ImVSH2`^+5yh#_ zI0@AbG%<_8!LU&1StsL5=P1poVG*b85uhjr31Oco*QDAVrSJPZWHgX#Xuxl z+;`V#yNB|LBa!pzMF}Y9{dLEt9fIaY(Rvz~>+M0fKYne6j znO+|DPn^v1$CzO&?!6P9Z51^;C!?1xRs+2u^SzYJ?7CFOI=u;2QkHSO*Rdf~_)_$e zHGHE6?O95az?u)&e>lNbH}~J4O*NSK}%vEvjeL!d{V4COx+`h4{XrFW)ZFRzRj0icx&^%lP{S86hab`o8_ey8Y;zuj-1KyJH}UMPi%C= zyy|+QiJm(#KRf_^__0BG7hg=Y*Nom0cG#WIw)+$SZLfO){EhBz?9$S*gI(W`i5y() z5W+%55cC$H0*%hrnm~gEJ2>JEM#>h7f{ty!TN_($q7bkswl72C`uUTtY&F)mC+ZBTa2vs?sDdu0->DSu{#TURJk{eT^cghkXMXoVbG)z8 zIG-b%ec74V!=#`-aM=r8{83@sBEZQl)fNgPBF;54wezJY?V;v#_Rc_+hfeM;hc+!Oxhd4ChE3740=U-sdL z5ihi@UQ3%91;0f2u-WT_U|&F)jLdYORgy9i5k7V(e1=iuwW4Es%~N3Nnv@GW0ga7USH3}g-K?*d-HcOvpCKk14vNY3@*HR)Olo#uo(vb{DNzg3z3;*w zeO_4<>i(e|h>b|o<72}Kakx)e>B6dH zS7dLP4cmap=RGr)>%3@(el~dN>2Ib^o~gQ+mY*g*Xe25Vq`9Bg+ZLe-z(&fv#Q|M% zAjp8xkZ~3e+;i^l?{{^Rnu@L;pv8l zK?m?w+@Om_qp~CtkN&s_;&H7FoA5HT5tYMfnVt&-`ooaJR8XYtMjxCc19=3OYd1_J zVL*sU#*{=-;YFvyI8#{u@lYJ4u(J+(dCVviU&vkY;y-1})X6D4%Gj5oe<@20YqZKr z3d zk@WeLaoufJ>#zu)i;DzdRwOBAXB5G@+1=)mNzOw~a~r6G0AQ>fAaObAit3f>k-7P= zXdrEbG&f@ERd+!kv9VW(oOB?qMh+{lAH@sercu^vgoIwiFENA}lN-Iqa}|g~5dsCg z7RKgSsHH9osd(F#4O^!D<{i)9+^)A3iy%o&n7`5?i1XLE1;Nvl;dVARXQd6h_&P!tO98Pim!kDXmt=Hfk^Y#?8iZ-+Z^TU&d~VN$9U; zi4OZqN!mnpqjV4tPk)_xslB6fCJd;@xVh5qp1a(2i?Jn_1k@8=pA?H}jWD0|sJvKb z9oZzP?Qe>=-_8j5h zcm|(jd8ad!p7#UrD=QJcI;)P5{byB4Wrb-Z`Tr_EP~Upp6;apkY~jlwvBLsA@6y|h zM(%%tH8(i@#7Dq57W0JAr8Lf@Y)u*_$0M_4Q;wxY&JT-Q{^_@~DQ?8?o?_eq%NE}1 zQYi<`g_YSp*A(xP`olsN&RushH>fa%G6VXgZ`2n2&B3cKsC6F^Qx^`{IsL!=;li^p z6*p_0bHXj{S4IH-Ak__gRpcQ{d^ z!1KW>sqtm+G}-uKk~jhJ_eApER}22ER_M~eUh0X58S4E@W`2cim22`ijk?Sij;W@E zuwqIDHa1EN;$}`R9FU8WhaV*VNCtdq;)q1+HXQ}DaHml%^pa~Cpm92rr-zrIJ{+Cw zGaK~H&?UGk{_yjy&b^UIMckO+RmCSk{ zO3H&61=|SOb&hp5=yfJ41>Hk;m!X8Kya97~=Tu{sX2Iy4Xy?6g;FQl~pNCz4UxUXz z(SW?lh#L+um%oC<1{1RN{@Z>YePA)C-~lB!lP0_h`qvC94HN{V#Lnv?GGu*Vy~f$9 ziWKO5eIOyKlut)vN?B^@f|=li-kS~)QJVFSbSl?!JqtnPS)dq|ppW)ekb%>jQ;G&h zA-OUN!o&kEOC5bNDI5zx$6P-}`IFr5FguAoRqLymVx^fZeY}X&M*nZi8;t#h_NnS$ z7?_zfu~=|pZKi!@oS-grfbulzS4i^(P6eigBtUE^sQ!1+Xv}xbv;W!={-@r3`QH-$ z$C=^5N4c#}xwBN(>)sWfEb+esxV#9ty*`nd+sEOKLQ?P}yDb!UO;Q1Vp>B8?&5M%i=;<&w3A!dNQ%x<7E!r&wb$rC=KnH)F z-}GbLT{@Qq`kHHSSSa~r-r1ALPUd!ogF6Tz&g%WluX=6{HB#GHJfOYHBul%g(M3GY z@R|Nv%>KTO3WV+2Jk7W@Lg4I5tVabKf11X021R=$Mh3`=Ilq$&NkppbFa zva-6g)>_hNr#Z3lgh95f?)10@x15vuRpVC_w>=Ykgz)=1_P9vm#}u18wZkqnt8dmQ zcQ=_hN-kL`BaKx>rxb3bl`ngYF>n}vxDT8fToHyg z-}re?kIC-)HrC&)zUnzbz2#=;xPr7l=GsO5xiOET#J8D|F3^Zr(qa55EQ0yDQsYk` zZc1P~q6rcnW__|wrUn-0=im9{ThOmqzKM0bz?Q}IipOJG9QJW`_r3A-bKXto$qtL1 zM>5}C4Ck{a+q=!?m-(!x?Kt36y4PEZ(Cf%on4l@aF8Yx}dls9Q2^bXS%;_e`%n)Nv z?d>Z}^^F@Om!f+W_TV_zBx4S)nLh~u*CE=_XI(uA*B}8OwE1)lx^y=iv8KjQyqELz zeA(S9QOx~vddaCYptkDP?y~;pkh=XSqeqr+Hs^NNFsJ45k+_1_;U&iFjx5@G!Ox=a zeVqIQoL}_t_IT%qYT>oNerNysU|S{32#+dhuzYt^PuS&b1&;tkf>tZQ&#MiqYNEPOUx!3(O(;c^ygzE?sHZE*a_uW1^OAdC0dO~lbZ zM!%-IjC;Z%&(89ozoZrtu=pwy>exYQQMGb-LW8N3i>g7p%NaD+i)6-dn3{D(rgw67 zA7i)pN3a52VCfoRr;5kT8xh^UYv*io_k(*L!LeR#MprSaXwaN57kDLiungrkWp}3E z*!u3m!Wtd6)A*6;i`JzRV{EtR)E5YWwY>Qh!&5$Bdz;&Ch3#q5D}S%sPTwpDct08! zOS&lJbge&lb!hqNCXLw;EIDu^zan|YM$&UMl*4Kl>&jzSrDnNqnCtxIFM9U?I1la3 zd%LW`i8R~BvWuWmovN=iI#w|B$D$7yty+OltdG6+)DbshotWi-(|?!e46-Qox}x9w+f!d5Zz3prHm zPs&O>fL0BnYJ}80sJmh*^w(@6@CP@F%1DtxPRA)-!z{-urIUh*X|fBbX^qcoX@4EM zDJB!azP+Uu_bX3wpzthG%F*-NDOGQRDH+E6NP&Mk5r~I=Fdx5ZDqW`~JINd%oTT`; zJ*hu=@VRAXLbs87kNa9L@Vf+Yi|HX3!<3VmAu;;fT;Bi8iZ7S$da-$}S;2I$2KQkX>FP-Cny;h~%CHVJ?6hQml^^&A*%bd;DF0$8?c zTD&~jp=-X|?2ZIglM%TOV30O$gE=m?D29ej3n%=nB8d1VU|?$^)jYns$2Cz&+s8}Y zadtb3s|Jz|NA>K8eVWDcP-cv8pq;DO31rD+jj%mX%H>5F38QLuz0H<>ZHq@(ipYUD z)+h>PLR)(Gh;!R6;}c+)L*+bl|2VP~L(Lk< ze0wpZs+@$vlwa18(Q)_$!!UX|6CZ}@yBztI??xkc5NnCg9=N{!MCN_}GWJPhxL}6$ zi*I?7)CYD>$d<-$Cnh<3XZ7K}Sjy?=h#q#YY#UpFv@M+($uq5fgTKqjAZgIRmzduv z5|CUI=QnrF9eU}#uQ)c=M8w?oco+k)vq#j80VULpTkkRwudc_rwhqGjO@n>0Chdv< z5vWsIqNlms+~|4CP{mZbtX31lTc)iVX~0E`H~5I;qkz{3J&4nxgkP68m;e-ApJuSZ zM3hfjD;zBxIxR`r;@fv+>l)_k3{=&jr9hVcWt#dIG2XAq{>B$Be8&KT)K@He>Mp{Y zSN!j%+1}gD5MXlXG=FrE643$wvO~pP`kS+3EXTF<-9DI{$m>?H^*zao| zqlTX_@_zBfN$kR&VSPT@Rq2(_1eVfOog@N?h!FK_E#E5R5gNov*;YC=F#@v`8yj=v z94f7-+!>9lFKl9fBuS`fmQ3Iy_@EQ&Q9DNIhwv4=G%LX-s9tZbL6E^$^6Wnt7wY}A zLHP;d;wCIa7#j9}_vkOK)6Xy@rM0&9jYj+V6Q{_J|? z^0AOq7;KEx#~4Ahh3`73ggd(^R77nX{6^VnFAfkUyIJ%6uC&9Syi>sZceIlfM6OTY z6|8C~)>=RwM`^)R%&Am;91Dn-3f`qqEt8?iqocaCGf10Jb=9AuY1XJu;K#4k^wnse zC67xM5!C=0=h&&!k+Y-cY+bgDxe?^-QotpE_bGcVx5~n^3G)mKw#&b|=(d7^HW5Hp z9L+kPp{n5exfDhHS{5nht(sQdE zKfzXGw&T3~(zoOm_Uzey4flz3x!$uS62WbKL}jjTcgtBiTS2ZTOPE@cc-KMkHIPGg zOPIB*Jt4E_-Lk1=R=X-`hAkSHF6&MXbB^7=65PYP?Dsr%+} z?*62qhr`Hkdnth6(hVP5@@;S9M_#Z>yc8Du=#NAU%7O1S)EG0NP+6>btxfO3{fr0n z$FS4~%Q8q}b&5x)5(Vn(3(iPYaJC%ojB6H`+RJ#EkVSa{VDm!Uo*`g1?JfU*npniyL2kMG6!9lD@8}o3Wp`v%G4Aym9L;-kD zzdb&L4XnR><3C_yE%9*+2tvYV`WO|7#Lo2z<#%w>fkWKF%d2%Urkq;Dh?rLGhvfYGej3hoEMrryR4du@Tbx zI>mS8ysH!pfNauw_V}w`U0vPbfSk?2v~VVlZ{v^O`jWPFpWx328`ujdEaubK=+H16 zqW@kqEcAot+1vY8mapU|!miQ{CXojF$xnq~iX2!U_waFr%x1=be?P$cpJ2lU zz7cA$p9-U}JB_Y_udq@tAhG0m#UGrW`pUMS|VSe8oVJ1XNcy=9=I4>_DMh;D`!Q*#sMi3q`2}c1`;Q0p=kST zjM&(r0Cs%T#h$NUnHiaR+?>(_O+5<3d&Lj6y=zJLX-Sh^Td%6c_G#WA(&@!U55;k0 zzm8~OQ~tP>hESk$tH%yI1H3iu3g&b6x!bhQ2eZ9=oA8>CcC}OCHiK)i=j>3M@x8+j ztI-+Hggg$0DoN(r$%K5+<+unA>r@B1$~ezlW2HSVn2z;ERcJBYuWr}VsvGGH z-2&lib&`)w>r%;So zBK7~3GWjnV6Ka{zFx!86CjWN+yWq+Hg8W}W*cad@gX*qjD_86UTO0Jyz78^>G2NZx zCXM}pos#1StH!oG&iBjR#FLOo7L2%Lgx?jYG|rHnouxKP{>tniL>)xv!oIv+|G9iP zOB5Pb=t(qjYsRL49M;}()mqO95YR3uqzw@({*O$6pMcm;LlpM5=jy>jbuX7aulL;i zdvCMiB!ib-^sqSk4GB@GNvUN#rrYxSO@rL$x2&-GYYi-<3TH2WZ~C04_FHQAeLW2Bc;Q5t=H~q0zjtNE#MU-7 zG2;<-KS14ogg8WOcjo_0rN9`5fr`gx+{eai)MBmzGL9yN!9k#)3TflQ={_S`gO;*p zguf_{#lP!>g9s9(#MN=5lYu1fD42gJ^0=1pUi=1BkT3ws-vx{nD~=mn2(++8-{?w1 zY-{(kF_E<|ofH6N{Pbt0m(i7q(*AcjXjJ}>=u&1h1i1M4X>`Mw1YDiK3Z#*~9evJ) zKWW6#_-%b{t+(@SvghY1^R%+F8lhT$>%1Pc+%|3}&kT2sL*8#^WQQ6w+yWh7l+;pr zDrwmss7%MfC#bEbk&s=YVO?J=c@g$M{m`gr8Jf-rJ&J`-QtbjMQ&C$v9`rIXMZ^X0smKsL-ewSvcz+n=!iN*2C z(?Ih6Ux!C$^EnK^SF(k;xE8!S1M-X?kvY0*JcI$jDNS{aJAtDTEe+6gMT&*XcG-rJ znO2&PZnsm_5>v(>m)Dwazg8#Uxr0!b;;E zCjsx(57{{@Ck`SU4>>{musM{a{j930PS+9G!a~!)BQ|V=rs_FW=7Yc5xsMX95aS0a;T5`@Ag*_ax(m-@kufg{;e4 zbU~~zgaKLv+O}Mj~@4#PM(zLSKo&!6Gjl)}kk zqEAuPD7pfhZ=GIhcRrCRDfUnn?8K?ed3BMwzY6fds|a@-5^4vG6=XxmgNKEe&_Q}t zZvcCX8!m*Lw1$v@**)PF-zzuBOua(N9J|K2XKV|v3IGdX`%Ypgd*-N7&L%Jz$On@+ z6g?l>T_RMWWce~uaJYiDYlA7x`o$Ww-+=*y&@nUJ>8+4uBFd15E68tL3rwZ@I=-5u z5QvEq2xP8fl>+(bF+{@|L0cd}al3}Ko#B3HYJ`_mvQR#RO36wGe>#0JkD zZsf!MY-Q&3O&@UYaCr7}EE$19JQUbkON`6cr0W|MsnDI3l3qEJ&E(pq31iLq-94Na z<6QR3KE+_Xl-SN!*cTYFwqB}>hj1}ieRK)Hus{}1N)YdZxgR-RZbnW3u{T%!GkeS= zf8)t@A_BuCg7gh8*Lx*d;RbIgX~q6K!UVaxF+}R4C9~>CkG|jmJ8paC_~^#HSqew zFietTsE(Spe$xmUE5hXYHn!cx;_VnpTFvYuc(F}7W?UeXjKmYsCto$`4oQcJkvS#B zucHXatTA6)Y^jqYFNP|H5vCH9JhJiP=z{@&K@lZ4i2xH6Feau1!W7*df%zVMn(t>c z4R%AAhi%i9hNmWbIap_%KkHDDu6uh}tl9sDj=dK%k;8vA>HS!%5vC6N!3zh9qv>@xqR=B34t;=rj+E8&Y!goV*C$ay zKQ!?)rp>KE!O&n4M3qQj4QU}=Avj<@DE}uAk-~`;*@BuLH$>%vA&v_?QV{d+G3tf_ z(Nz#o@6K$Lae)3`*OBKTE25h=9}Rj(1T$i!>8ePN!AR-i!M$gj5Y%JkL|;J=?F*hM z2ZHKV0l}DrE`$mU%xU=tiSc0uAjq^DAUJ;Op`gj$VzBY|wkqPB#&@N2pLUp~yJdD~ zZUfiOT|EuNg9@Y0u$g}a>oLyE4iRdN4ir5&Jnaj*tyn*^=U`25JmSXb!T6!R)a~UOKGOEA#?)|25kcNf`NUHDkCnU z?zVb1yJ3f|g8BYi&Y(DetBONT?yJ8heH#E59tCtDraQ(Qt@W3{qZr@&or!V56>pS* zwxD+aM5c%P+kI9ab2GzNkIy44rsj_qyekv%gNa|b_eKsD?!NI5p@ww$SRK^?2>fr%tJho2Z@H4W;Ev&>tr zjljqIoI{&irk_3+uIN-H=EH${fPBB`an1=p80S?LN<`6T3uhL=e+aKYt5G;EI$Eyi z<|vqiUyF@V*uD8+LsLUIBuR4m{j#5$hyjiAbU6xChAf4$sF$hiY`YV0jT4qe#Tyl1 z@(!Kkn6GFHNuBzyTtuG52gXxv&AfKRC+tS+m>1ThFcQ^#qhDBp`h)UF3fSUQVSi`m z-^ueDE;6?W0cch}(pdfKpiJ2C#BO-;V(1YnD1EGj^>!ZOhdNv)D z`+IHh*)@_qS1Z5}zDIBH3}qh%<%DCmD=EA}iah>MVk`c;SAwV zU){>?&~T2If!M+&j+O~L@i_LsuRbtyf6-4Ox0}U}5luFrX1cd!d)UBd^!zlg=86}b zu>2;A`y+NqDFMscwGVwgiZ*a&iu+R18P3RoqD1tCWrEe8KDB$T-E}E7{V2UD9PM#B5TDeDI^ zs8z0IC)A}34J{km??0|0L$zbG%oAlBx%eENavL-U1$?{b^*OE_Hib}|nO?~Lrzp=kB<%zq=awR4@4mqUiNu45PHYMy4YY|uoRG`Y&!qhBpLwD%se+V} zD91=iNY@%ZYc4IG+BKp^$Gp`yUDaJEcutx8jotuUX9YCgDXi=5FLMfhnwwu`R#x6x z=M!iXZc<%#Z&~H(dh{`s*D|0oxrT+6YY&sVat6x3L);^Iz|G0xiiuL9cs76Rqh|h2 zIADpj%qrpr`PRI$9o}8r61x$W~cbX0d>V3tQ(_k^^7>r%&^GMawT;n4P*&^nd`BDU`8Vu|?H?nm(3hF6q z)+RV~omx6^jQshXkQ*0gpVS+M+a`wWEaR(Mq73Gc&{XGE>`rUlC=E8H7SBE=1=7x- z-#VfQmby45rh42HrZx%+7VkbQWtcG>#WsT64nY6Med(Uy-C1HB3eCYarG|caPR@@u zi;^dyoBcBTJiX=Fl|G~W=9ZcPrIOwR2mW#-0It*!ZTVkcXSpfnpe+09F&>s7%uoSu z&-aKsGzDSGF}%mSNZ{vdG%(4%>RPeJX$T1z6*cW;FB>EQn#^wtlX&60{}fDOkw8eyiLfOIP(JHbXGoAct!6J zt~5=5;m67eZ*{hqu=nDyQiL82lu$@=@%fH?D6FDk+c!V1wmzm9Hve&ex`cPb^$m2b zY%Pq3&#cj1#;KCtg8yi9wttq5fjEhu{2cU-$HEKUG>MwOE09aL+jBM7RD9fNntTcd zlI9@6KZN^Z%+KZ^unz;xY$@vlH9ZHLfM=8LH8ekG#5816ZcG z>28VliAc68tER!ML6f zbMz<%p5E5il4nT;iwnQ96JnB|mF1x$9D0<4J&&Q;hkdYnN>fD9Q~05hq%(4yho+N+ zM?J6@Lm!DUMOwDri!96xC5|iZgQ}XZ+EV!#1PK+^+m%%11WmdKnZE>wBE3(o))-k} zWNKmm#6g=X?UJ0L#m>H^`G;l*sYS`qP3ess>Fdsb99$FT>(1U6eb-Rt_H!d7K51aQ zrWTX6^&*;;yHB&Su0QcXhT7d~yC`M+)FhseM(+E2^5Hwd(h=mR*iBm-yMuh9iwya>S!x(#6iQ7 z>_2IqDIlP)Ds00TiB-0+_ua@gZwV6U8xBJH`u&f25OMu=BwHK<49+V?bc~j^>C&6_ z(wfk4Nt*Xx`7<)jtOwG+*Ux*uO5>G2kPwCr@jnL~h_+ENnpfLe?k=Eh(3v0N)@l)k z(qQ`hZ1GM#=d;c#1!T%bxsP)rRZ%;Z;StLim7K%wo5jpfBS+3IEt)6#dXjMxhk9LVKc_aK7yF(`|uDyOwZu> z<7qUFGXlS0WoMD=dqAA`&{n1)C=?puUv(6t3eN_r`+4TZc)Es58UZZSk~4nH=!nlT zAfFrc6`C99g8OcJy1N~qPxPhE#?J2d*_qd$>);e<(X#OYlf{kLev{s*!$4mtITD=- zYqeRAAt+v2-@X8d?hGPGVnr;RuqBJZW_sed%vS9C;BLLM3AnEDN_2 zA&0C7&QJ34#ta%QA*#kC<0CbeF2hwJo|mB=orgssB|x{22R~)tBnhxe-|hN=;yj;1 z0~qSA(S}8Yy0Lm`_`@Sg@rf{1OOK+2QQSV#RD=v~iSH;F+ws>^}`DS)|&uDxtc|QCOhb2V*F)! z)8qChG>=!1*Mm~HI9z!==KZk1>b`u-Wfb#=HqnAcOVN*eYILHm8knSgC~kB*xd5lQ z&t%`IZu51nTSy*FDuh>ya#{SO;8c+XK1?};aao3WncB2BpE01G1ZT_m{lSu*IjUQP zVSJ{dPV+-2d6Lj(H!lV-zgm^1`BnpPB_rmcP)`%~V~8=F zEHa6R=m_r(rVpiJw8+iKp(^i__DsI-onk`6c6)sd!6V13jiI(#B;pQOvm_(IvzU@U zfSnh&hRsH$%NEomn-37gZKO}HTrFP~w+UAgc)tyV6qz4{FNnu96))_sk7Re#Z~Kk8 zmY3@G3B=vd$RCQ=Q0T``Gh_MkWa229Q#G~hwhBnhW?Sp0Gm-DuXE(n7C#vgD3m6xr-#^&bhpr7a)_*9b{YCT28Hzd@u%0*>S zjjX78Pzk}kD7>GES>NG#KEF;&mFszkM}NuBy@D(SG30ptC-m35vFg{$A;QNgI8C#j zDlXPF+cn{W`-R$0ayb0u^yaC{`XlC=a!U*TFNLb-)h}D?M9o#pzq`jYQ+bAY62#3~zy%CaWEy+2D7Pk`N47HjMrW`zcFuS0)#F3y{$~364`!G! zAJtCT;Zl!*97h!QsAG((ey?H`m8)jS3z`kr+>(hrj?ii%m(B0Pnv{04Y49g>qa%z{ z=cGt&Nt+-tvd=Lv^LFc^7)TB!uXLq#+7nod0NHS@riM*Zx1*46MJ5)TzQ#kGj&u;#g^t zUaJ%2SE~Fj#HMT&-WwiG;V@byNkIB=6n#!*n)6;rG)#0TJE|ovDN8Im15au!<%6eo z=GOva4iqc(hL@X0j#|PXq!#649LPeWbKQB?|%kL{E&Z8fHxJRZmd~qe_n)4`2O{MI<$oo4!sR)9EEu5DydV*@8SC-Y)TlG2WD6KrG(nk&*-vggQf}t&IJOop1Uu}*7Z~0B#_HhP6LSW zY~Q@j1C9W&3tBowxOx~|JU6`fwo}zgIYtd)D1%luM?CZ&<#PjB4dv5sTu3Bxj&R1c zID}xuLUk7{rhHQk*6fFo$daj1Id(o|x}HhGpx1&42y8oAzMoFg;csGj+#J{L?XqHS zXRDy%@UPiRvs^TVEOx(bzqO=L|-!|7~B{U@N zyxZ7)5O|L(G1o{OYuRMk;(q%2@^nR15tNV$ck^MOck6M6y0&B+d5WFMRxfHUK81-lefKXY-`DPKr24i-0kERI}<s#l03t#&E~+AC>ZQV($5qDE|TBw z+=W=ayp|jRM!9@e&nm7GC9ndP+)L~SE|pn`ZQt@W^p@5y>rn3aURRDjKYTb=5#&tL zWX0dzF8h7|j;85{nKe8Kp%VbUOw9>UWIuq!g%^up?`ngx9bgoHf3iKF8v8tAT{6C( zv@fTcp!I}4D;3n8Z#X70{AaryP9BaYT)^J@1rl%#^QX0SBJ88BRi~_Lf9Vw1a!ITA z^5@cD49NzrAM#Vzvd`f=AclhRRZSBsMo~;L;ZH?VE3V%2^981&BY1&LfbdTY3$K4x zXF&ZVeQx>%$b<` zAMtMdw|T=#A`Ogj=9BGxdsPhtAcT#hd734Lw-Ugpr%X?lgfLT;CiQP}`Xpsxe$fDz z63m*fDLlyyOBfF$yeKJ4|J%KJ{G2dd#DsI$KTc#3CnMr3$Zd$2=|y47td3kRO}*bJ z81gHyZ1;G<+55``uiQZY(F>a9Tf}!$dU*&BD*(jGB{BAU5td(P{O?Y zB=oeFif$At^cO&(9-Dh|W|o_hANQ2%k%9%6$+1?aMua}A+aC|-797NX>u~uqV}!$` zBc)o0#U++wctrIMxhKbz>U*r1Ocr2BF++wfZ;B>hu*Cc+PQLCFTq^<@CL9wZH0teR z%+@Ax#GW6K{pRW|Y#2tX3GS({miM=>k!YrVDE#brk1jOq_=|y<8qXi)fU3n{d#oF8 z!R#X7T6h|SIe}*fT>b%1Z3U*GZleE-jDs*|_gg&92_EeFTZ4ZgzBGo>w~)_==9$Z- z*nECocGsx5sv=!pTe;Bd>Bt%%I=}kXjWoO~aHBF@YA(^SsgzuwpH8w=uOn)kbMEpnuWst{YVe%k|4&t_AFFFFZvf)JYAs_ttga74@l!vJ*NFPHo z?D+6;O-JVgc_557Nc;Hf*i-10g?? z!p}(nQ7I2W(M>>n!cQAIL0nt~jHcYRce%rn8&7x=eaUZgpmmlG&xhaVYfbvxXicJZ zY`_&?Y}uT+={rnlidq+b2`1A^dv#!uN_?%~@)=U;tgMT8{$)6jq1#1xvEY^(1n3j1 z7_1=Am41QfqauDaceaYk<_v7X`Za&K){UXTvL5%aELQ(lVH)J~;IUQkn6XixkHu_Y z*ShRhoL^43{>$M?=2p2c`Ga5eLW7a>h1B&On5?*wB)pyxo9M6&ZlX`P2&h7R%+9D`&`cuXw6 z*MYy($dSL@)!Ncupv23hcjMifX4O`+x!B8U>%=LS?T@>zWV@A_dsmB%djZn2t;Q}o zY9;Ca+A)6~;jN?B^%fyYKF%FNSZq+IM}zz^uo`}&KuPp}ZRo1^rkdaN?|4%=1@`}I zJ|8y<)*NY-vK7(dZkE@WkjE8_3Gfk&jCRA6GDy4P#6rQ>-KLYwi&l~1&2A0B5MeMY z8C1v;V4Dw`23i9CBjte_5LV&-qx|>yA8{J^KjO2Nt%_G|(XQ-Wudg8YMgJYRN$asu z-R);{L#4wAEwK++?`K7e8aVY?gmg4BOW-F|b^cmr_RXbAAjA-_K|*69IxsZ$#a&0! z-C1mPF{d-No&`)k6rl|#Xa(-A`5R%RVnJaPe+Frf(4Hewr&vLky zn3*1sOkAA!kdly)5`OUH&bj3O{p)l}Fex*>k?|4t@JYb+^km6?9owrpv&rm$gm?3@ z*}c}+?006=;BIb7=@F@tIe31vjVw#f8tggyuzY>7e`3we;reVmwVs=g(#P}4{Yg!s zAbO*+<3jfoJ2C1*oetBN2>86CdAJ(8yeP7arIa0o{Ygnu$e?*P zZ%}2e(t+&6pWXz}nm0{&M7WfWXK9Yr*#XdC1!ldVWA93nEO<=dBQpqcVKKX4%{^cy zQ!YULjDNGNyGXTC6SlAn3Nttcyfw z;jrI~@SEe2UubJ#uE`{IR)FjG$|*24WuEV*?SbrdNhsdC_9{PqCf&k;&|WlN>biT* zXnAKZM^HMLm%89tHhCT8cWw9vL-Z%6Yt3v&0SAD)8h5d!!Xk=Pv}HUgU&6;B&^?*7 z9|*<&tNUBHn_1us&K1Udxb3T?XDz2H}f<5dzV5Y8pd8Yy6p`j2Coy=px7bh&V+hG>+II6@U zjyi{FRDQx}HyZIURKoJ?9>!iv&zTbZ7#O`B@%!83lo*x4e==w3_%iOzJrEC;ifNKW5CK?RYm|DwRYrz`2P8y;Y^^#7 zsf-8`u7+>QQ{!lJDE*pFh_v6EK^Lg52jZM%8X*(J@sC6ePHc;W>jg~s@8dJgGS0%i zL`(5q)tGE1I%b;s@?WKC?{vEj@+OPe5Ub&%k{;$PrmzJ3nUOJW-)Y;Xs(#2@#M5Uu zV&2Mt`>A7b149t`P5K)hil-$t46>Ru&pSVrBOACT8`#UO#hE`hUbdb=Pn9E_$Dhs|z>E(rcUary!62y2hYP zbGjwCANvkK*mM&w6QWwF3t9{bs6~px zJE1*h`DyyFpT)}^3Ls?E85!MQ7kJ94Cp*2`dTX4NADuS>Al?*`vA9t9Oq(D2=lcFh zF63MoSiQ@%+R-FOW^p+lCQDzrKGJG2Uy#QF^zI8gc*B7!35gaJyBba42ApS%Zx&U^ z2dSJc#qBtspIXr}3H)+CIqv>;USw95SqTa>HxX?UX$9&^O1d_Gv!HF!_q=a84Es^q z;qL0Qm-9sG_G^0wy0MK zTB_Qp><8m7O}5>f;xilYd;TUFs6z1Ld~gCj^g&qV@!CJ|?`b)jR64#~49Bt9 z6as(P{}Yb<172eu(cUoNVfff(dgt7A^Utj9ji~_~__)h;tJ%8m9SbI{w@G9!|1Hv; z^Y@`=dz3yJT$Q4>uD5reX?kck0mRK{tsv4S`X~euwNSf9kiw1gQoGOvzt1+S#Y8Nz zmm#1mEV5JuGW9#C`hn@gK2YEDHcIE+yFkalp%V2)E>`~?Lfe}4TZ`xJd}Zf9<0G(1CUTs_l6+`!nbQY#$PoS=Y+(ul9t$3`G2Cu0+bKU$BM-8>_F%y zOU)O>fuCK^(Vhy%Mfh!2S~~!cW5M|v@k#1sB7m86t(n`Rz|)}qjXGR=mCYIv-;eG=*^m*tEzv#o;%L}EF^I;|!Lmr>+rWzDiOKp}GuBCd=2uyju#0IHvJv5}l85Kz-jS4} zgR>TOmTQl-D^f5z^ZXPv)&3R2CiPx~l1QQ?00f`opnUHH+sQB2!1yO)KnRSz^kb=g zm{|`=OR-^w;1FUnFsghPW-9&%X?l(rOn|zI3eToqLr({kj?`JW&e4>)1n7h4;OAq+ z5YiipZOeL2#C~E8iz)m0#ak}@0Tob7@U`4_L`Ys&{vO^G4K(3woy=>j5D9u}s;Hc_Iz?a&EF#Io4^aNNrY8 z8{9|85g8;MWM%0WS};)tG0(`;eJU47_*j6%*VG_3Q6tg7$jx|8_xX@W5+K8qcMPMv z$02NA3wy*ys7@AP&BSfhkVcodH`*7+^Zny~%WiR>_yiRI`?N6i8pQ4_FgG(+lpGsy5VnYne(FN0wq@wqn_h#8$1Ki$jCyNvSF< z==`KIGebqID$ehD*SIt$^@Feddp#ifn!qw)h}8F_4}jll7+E}~b@_ClIja6=t{Vu; zF6&$W8v{H|RsoR243+33*r@%{@*V4f$~c}YxR%_{UX83$=C1q5#4ytOXYTLeGGeTa z7aPH`84gv%o(2grdkTGS+mcgEQ)H9!a*y>=TFAHlT(Yc*!dM|Q1XR@uQJ1_$1+s={ z^q~Bm1~})L#0pz3um@`LoOaE(+E{ezYb0Iy6?p{1_+&+we&B~tE3uu6Sv0?IMD$-P zXVJg$R5sa0gVWJzO}5O+ne6BKFpbi9L+xrv_QM(~R&=I|Z-3Q^{r_BNcnZ^jM?U>?edGG`Yi#U5F#^pm#sJ2OF6h?BqfU%s zk(A@|XKkyE8&at&Mm{Y#a^7GQUB*%-N7xJrOlfL4lZ;lR6Q^%8zqb9E7mG)gBFC$! zhhe{Lv((N3SEjG37)0*89&svZ__)k6q4JyDw0J#pWr34>3JPS0Z>c+1~r;_YDrt&kg`ew@#cHqt6-^ z80n@HTWt2dWWVb3-oA`GVb{x)6j`_<(moN4Dg-Vbo}_#c%yaKlXuii)tf9ey8T zh^3VS*2h5Od1-jOk7tjkoOfby3^wgfsBUvugGgu9lyLMZ}4Y5>7)ASa0H4;yv&Mef*g0H#G33FPmTnWUi z2lKMI_5*{qQcsMT8sbstac%p63JT0{lt12^It_+U!nvH+5o=;p%U9mpF(4X5g{vZ8 z^wubFT;nt?*`zy_87Je^Bf!s~%0HsQBd#kaKntpn83Ec^vofxEcmcR0dwlpA=xoXg zL#Ts}?|wt~IR^cFEv3aBrhYt50eK$6k6$U-ne1kX!A^R$t7`(w=!Ms~jqLMcBDKk! z=io!vv*kC=H0BI?4sSzYa_BX8+jVREw#ydAr1G4Ox(~ys9Z#ZCR&O>XQWy&`U zE91zd=PzdT|Bd@UGEJ?7ER^04{QprCmMQ)9 z^~wdyNXTlK3O<&?H`9x0Gi5fEd~H<3z+=iVtYDXK3ap{{56m&q_mVjv%zr2n6iu>)g@LgZ z0YmSRLNO*N$b|Y1`Yr$pG(mZOz9<96UnBk4!I|Ht}GTWtgfqM-M$j^_LS=u4dYyMMw`@FC!&-u=9!_9|Uu@qpCjX zfE?b^j}vaLWQ*?X&Wz5o3MVX(y8CGqZ&-5tK#E@xJPrJ3)-*5=WC?(xUba0tir{ZE zP@vzR^y%M6w}Q?jt&2doy;s z*oQZ7bEhI+J=yxbiNDv$JW+rF)#|lgn-<1bo`H+&vd+b~(b8GM+U3V|T?Yo|r=cp4 z-YD#9Zxab?I_M?v2ti>iBKy+H^klI^%_1a3a?srTsA9otBetP4-;{uA4`&gdgA@Kg zr=hLxp0^>?4qL81pK`wh_N{&X{N7W5%=W%?s&f2bxk*BCD!1+1uhy@l#crM(z(%#M z7bWPaZ;e2Xk3kc?FSxAJZ`$VV(l+01Au_c(sP{+LTb(sTNJip2$y=t!OkYEJxml4k zIXN8@)2G@R9b&Le`|yNlV6Uncb0{{JxG7vq#!6s_wRQR&C+X7ui10T|6T8cji-HZd zQR~J9ukVnp<BllwX#-deAIZfh20%iJ=}^=K``fTZQP z>!?hG4RT!{*8jpkSdhB0FYm?fm#6cV;%3HeGNgOnyz-co`9m@4LGX&L5sQLEQNE2Z_~4j zl67PMRZ*lLTcD$2%ihh#(P^|b)GitmLT)3n98fa=v4M5c2EZ>Yy(hRMfq`X5J)6Se z@?^+%@$9G!k-X`XvJ_ll{=iK@u^ZmMJIgLkAqGQ#-69b$#L9z8i5w(~q#`BhERRms z5N`pHv6gxjA`E#tMD!0^pVQZfv0WCW{yH#&9aEXAvDn-p7=c$|b;55TbbFTh=iPe@ zZwL2dZ<771esfS{CIt#1oOHgjK>*!#U_PsH z__8EFCh89IrfKbK0!UN;cL&XZ;F|?c8u7Ty5bo_;Kft3#nD;df6vubQQU!wrw|g3M zqptNSj1q+y(}di=dru@^X08T^#JbXM$;rjzm05#e1n32BEq{JU{aDBv^QNW}uEWsE zssJ(-2?*Foviq2kV)l*&IiRG3&Y-S8K_NR?dD={8rEXm_r4M>-Q!a+f-d7~oefzTR ziF`2V9mcgcS?*GY!(`44@09x>E;z_MQlI|mp~Ngf^7MpQZ;fp*oE{Rw9)gQXLXH{@ zn)Z8k=vrQe3n91e!u27SOa&XJdxM@64?}yq&KzzCtmG3%%^}vPu;%r?0eJnLZ>{;| zg+HX~5DHoPWJeV-wmw9kg@Q0UUIOJs;roS0cKQMY*uj1+qAWhn>=wqRfGQEzrdCg} zI)s4Gop{WY6>h>-y{p`h1S~~JgoDlf13<2}z#Ke9e)or}s_NA23P0oq*mq)7Q2+}X zU5jQIp8|J4VVma@E|?CYY?HrCIhr2EDVvR7VYPn%82b_E9$X-n7)()JBmG71?|t?s zNeFp0uOJI$Q0=MN65VnDYH_rdNrsJ0&!#{(ssgubX~~?u{|sK)XLVsV7~k`SkVObO zp-b%v`!VLq*5YQk_Mg|jFXP`@6cXqD?$PvA4_7F8mds~mB6aNRnNLa!P2_}`3xNqR zVPAPqy)-706Y4mU1PlxBC)ao`Vl&gCVdG=Xb4UaWr1aWmme*H9q>FmE|47{tQ0mFI z+teC5Du)!^lQXwuR|Pzu4<8fICPW!^>+irxqr;Gh)&?5`27nQuT@%3KtI0s|wS&z% z-3hSn-Y_FWQD%&K!;&xnXC=-CHkIG|UT-rqJCWrSXoH3aJ|l;0lTd@ihWO53A(Tu_ zHDRnhC<~2u?AWq-=^q2W)FHT(x5lW74E!0*|NKx;ivW)q4$>8^9mv&CI`L=P3)1P- zgNMK*rxiFkOtx+#RhW7v=b<%SiWf>pLI*dWEsb zDc0ff%8g+@f0)J0U`DITHD*G~6!Lv3Ih)~T?f=6UtTO-{v5yU0Ha_!LVH%um_4qqF zrGcVp_=5QP)x*1T*Yn+P_bmHKNzH;J^sU5pgsafaA4xvl)CSw=om#T~{I8Np z!tB&vK<=YMGyFsvIpCh8be2j>Ls9^F-gmvjU0n7&VL(IapaY>H`I8aZ?0yjyq#=F~ z-z|f7|GbH|^wxXr#(VOq^GM)o7Xm#Fv7Ao7iujcNH-WzxtoL|T8c+Zt8lGNT`F?)? zCsp&VC|Z3&{DTmYfmqr-!7hJja>G-I@0+EU7j)A&#J7vlyZZjS4eOoS1u?$>qf}=>ljrnGIk}yY z;6XC8*|8-j;AoencqTq3eI%Cg!qw@dl|o@i<%z-ZsIyb%XfYmbxBmcm@570D)zkTo zVO;c-(Ng|A8O?p+Q=3VOvHl$vE$1BT?WPT#9MR4ihzRU(LgS6=9 zYo||Fx!sQZMr&4TwVrcQdnEAR*xBOdRfe|IID_Z=hnl{L7jg0hyYI}3{OBj@!(%M- z;_q?TUCAvw-gX4<8q3<@H$<~zS{dGhxMa&&l8TX}ueM@uP}_lwtEG~ylPVs-BZA!%{ zi>ndN6X(BE@M=Iah8=O}FdhVTbN;=!jxMO1s6o^a-720)NngwL0VlJvxXRX& zPFR(DhmDnUes*=x2)+r%?``JjhCce& zZwPD>M=R7W9wcd_A_PK_s9v$i>ZXQ9M*^8(@P2zMY!i%j8P5-aeTR1{ZI;f8zcmIs zMpG)mHfx#zP%r8=l@SttcsHlq_Ki$^9S-VCm~Vi84o#zNGVDZvv4j0*71Y#m<%5bD z3{3gL(O{@sp~QIh%6PU=+9a2Oj6}*NvtX%mHtAW=I`zMK{uhHH6e)${qt7pzbeu;i zW)GFd7KWy{9Wc;ow*A31od|YV#Lm*vOxRm)hTr031B=!v`T5~W{-x_b{klyxfbTU; zwXsr;b+vhAzZPw++``q;Wt$!!r{m`=Ca0D*HAlCdhxDvieSogsUnc6Kd1u{0@) zEI3r{92k@EVBp59aB*-yKOmI+@aMh8@%fF0y2exl3wx7t8}%vBcm1V)S*7lyqyFLF z_4=c(+?*WE%f<80;LXiW8#75sm&w%9DR~72`sTOoog)K1(uwlwsmaw$7vrvsFU>14 z4{jZNAYQLC@!O~0%N-VfP8#{z&(=GZF37q;hNSDpjCMv4uy%fEU4&e*8N==UL#h^E zsFY-xcGv|_gfS_nv9rc4k~u`a9K5U}bTyM6nJSAx`o~Fg+N$L0V(-_4d z$Ew9;v#I3=uZtFp)Q$SSBYxL28yj)TczYp(9z!~NGGFsWU?u}mw4KtyHtsAf2|CVO z{0>`n91Guf_7qSa|?i*omkhD(byKw<#WclrHuu zgMJe$9GDim|9oKI6Kl`Rw70a|yGG%--~VuNOijk-p+r;pHEhC9F5*R1yUrv|?D^cy zn3IeSf7e?Lr&a?ZoJ?uvx){(WlL4}&JXrRwz&HV7|3VmrCrkqTpy$9uI$KVL?alK7 zxbS}WwA+g3uo3Bq`sn!T13Lz^sj-QFwz;Aq=s&?}logiLgBXqyyGzid&E#1aEc57X zuD5?UGH9LkZT=bFfi*(Zx>XZg)9RyF2)_9{Tknu1H-^e+dW6fw+>%!{WuZ39j!zPa z?9~f=o$`OCgsqFG{!rSF{gp)=4$>WW^H)%G46pEcRyIsK1V+^z=uZNAdYlZV#ZGJPOOkzS zR_SEQZcLn2nbG}JjT1CAb%wLvR$B``S=_Yu6{oSw2AvTCe00Q=HX_BnKx;R;z(OW) z&DD{O3Rr8XZUFj^!7%5puD3&BO-0xen@e#$$ni-^-sHF?lUVrc_I=)2&+7}dj+m4c zs?g9Ww|x(a=?2ShUeCL~pb${2$0X|p`T`d^8YA|TAt20Ogs_4f(_qW>e&V6HoGxnb zO^xMxAWE;Yr!H6ojftH=L{jEs%7QVW8M3I^>oRLdE$7%*NyHB6#5}#RUk?>IrpR^Q zy5Lpvv?tIx;R;-6;!e`HW`w9i)q~ta0v4xkI1__9vq){GN#+=fOo4>p`XjvtmnB{y(g zSXp*b;{5!@5!q=vw$O7YPE#(D@C#MvD?Y>oPn>hGLdm`4awL=Pb=-!pkaic>bCz@Tji+F?a;HFLF;XQ8^d?$3CJ~cUs;kc$F{I z5}C%yqs?hI0K|#{6OS^fuHnhokqIry^M-&;9JLZmIjm=&?(WgTa_5Wy%3r$5j#b$NGQ%6xNMaLwSpycwEKTBt#` zwZFXec}W!BB!K?eI_wBZ`@=qT@PZZjq6;Q|Az;nGh)dM0M<^yZV_(qUM$L#fr7@GM z?$%aT6z^Q!7P6VynESyr*h|{b!Z@YDSlRs-&86~HzAgeEkNAjE<)zs$awp!^CVQqE z*u`QS57PUPbB*I-%H^ZjPw1kh!giEbxt4zkJ#Bu;qY4!D;;#ovb20b1-j z+ZeOLQGp4)a;-zMW58xyHJia`nY} ztW%L5;t*$OPy3!C~Qk^&z0#7VGKl z;b0!7!{;UiY-*br>-2D@4iI0!sHy2u=j#9#T+37<9}%Mj6}FxoC%G~u>&*21%~w?w4_Gf0149l1FWte6c&>FM05zB_L0$>fk105H24HDm$X9*{N*FW7-hTLh=(!~T+Wike4}d&Mt*0Wwj1N6a{6~N)c{ck0xO(fT zHiG_pnBow$&>*E0C{`f2ySo=F!J$wfxJz*;5Zv9hl;Xi1io3hJTahBa^!c9WdCz(O z$;r;lj_hnUJNI+%ojqI6w2&=?sCGQ8Hq`u$&5;!1*Q^)V?g-O?Xk+BEp={Cw`C>$b ztv~uDfcO_|;jDZ1!{tj&IP+T43$y;lDis}T4xe|A`_mrZwhio6y6E3kJk>v`_y1of z7dO9#gs@Eh>!yVb8@Frl|3UW%2&y%pJKz6x4NlOy-7>UUensjgP*gpp4G z{2C*i#SW#U)`$NQo{k*?e>9z1Tr%|uIII7cBeyR-!7Ra5ZOFdBbVcwFm3LPWOl3$I zRE<1NK2Z$0$M_d2_=nPqvMib;zz|wifZQ4q|Cu`kmU^j)5R-qIwZZ8)jBnPaukU(J zUed0Go}AqKffCxaL2cT#xugp$I7@k}U#TOnAEdXvC>pB&d>~v6rE_W4(=rQiA*V*8 zu(@FQke^>04E0vY`gV8yBNpe@5&P_t}q;cE^kN)Eg|oM~6)ACxt?tH`II@k5iN zz3lDpxfxORFZ7Pw`+~B{pKGPS#ba3GK<2o&uN^u04I5RNswc+v!G!Xvk@2wpH1mCJ z9a|gNH?G%HweESkWsO3?o^Y+AI>id#wW)9`%dFMU++ejz18XK zs}C;?Mqi(ngL9LDodC~6AGG!F&}~V`+G?vwW6*f|=*WdHOJ{%=1)B686I!(O4Yh*J z>&afnmz;Q513f9X?F*x6m(xdP?fq5D9;8ED%~zw5Rt}(8=fA)Gy2X-?9VMX^C=%l2 z{ZqSQj<*4@uM=~=aWK_Z&@cUV2di}mXAu`mLQN%8i`R=-y&?Y7pB1mYBbmIagjlTt zP=+t^9=DIo&hY*UEVRxVx)YskI6U0uEp%(;y4v1AoqW=w8MC9je*p408@sW;>Pl@y zk@Lqn8$3B4l;v+>2*i+iuD0G)QqosLc;U|cvj)qr>&0e_T9k*Pmo!w={f)?6jBU!* zvaVxK3=d`1So+~-Ok@V2GzPGl!TH$e#&ZE6!yJ*89qxD{@<-!W0;zo68E&VptKT-V z)`V__ad?ESzP{G*w{wmv6ZXvx&t?c@7H2D~Mwuhx{em}u;F6mrf~UH8M4G1CJ#O~Z zJ;jcb{}1vLN@$l{^xRy|j1;=G3tgbOj@sMY+1bD*5%fn~}a~UEFzsxR>VRWxrC`u%Zx5{du6Z`CbLsK?!$`tD*=i z5swXz4*-Re!wCYj#NeKJs^36p?7x|<67*S_ItWB?#5m%i$s87BS=f{|crGfKp22wg zny0vWU3hJQ^O6`Vczh*`JS{%x{K0d;HKrpYDq#UF+WXH&Qu*1p7`;#0z6@91gRo3z zhXZ-wo34CIpA8%FpLm{Mt!9lmQ=8q6`6{hWLK04zJJ2V3wr)-ZuGzY2Xp482Tizss z{*c{;R&5e+PbSshIXS8m;v40Zys@4wv|a1PoBZB<3ypp!axr`9uJk$BJNS=szO&_k zK-n)drF*{THVKWgi|_e<79Ne4t6iVY2@q*nS39T4}Rv? zBi}$G$`~+PM^fTfG7oo`q+3%)`N76th-K{3P)V`6Dr%#TB#ml9xRdizr*j-Nc`5%q zY9%?nkhe15+bWyznTC_K<599Tq|nZ0>r)=`Ux^+&hl7oSJNJC8zKrw8pHp|6m)ETE zw+fuYHS7vJW$_t)!#%(vTWC{!W$=vop29uSH%wU021PqE5LwBCCaQN+DgtBzVOxWU zjnk-gPEs%tG3yVE4m|z2{yYE-a!n#m=n`nRQB(x}p0a5Fdp(pdX}f4JHq!mN6R4+$ zSAKa3sCs$$E95*I+AxDb29#VWof7fS4vxT6&@jIZi2O>+s9kl0ViYPJs}#+`+% zRM9bjTiQ5r^g7t2>7ul?=8WbJu#sI%{usu`kANN;G_5mj>(5^aY~e&l#N=Z8qEK>v zzCZOm-v2!3gX;ExhI-C=tJV2e?xQ=^ozd-c_hu3*-^+;Q0llp5gKccRYO3^?RjRG+ z)Zr^4sgFnI)%!M-K?Iqf7Z5j)98mqm&uJmhDYQupn!+7=e270A9u^zOCQ~0DVq7H< zc8ZA4#NC(B+V@?#7NJ))0nt}9h#pjaf~_<%smx1hN9nv8phNGh%ukHyjEA3gaDx)C zUtn-ZT9@$f_n|R9m&{rCLL7x@`)z4uFszrgk8#ykQE~se+`mou3%)?4S1(PVM0b!y z7=GOicE}%`=Ri>dzreHM3DnOeog2nt8dXWYyJ+eLNB>cpVRdFiM$6l}e{R~KeY}hx3ZT=+O_rp2B{M;i7c^V*KZPIJsr)B`- zmG5k6_pw(y*@joOwR_(f{^cPlpbC0|<{K(_A%iQ$W8I7$snKF8@%gQ_x_(PuzVBit z31AvcAJUSn3?AfUSjejxLRMhod{_;X!Njy1i2B6R`9(X22ys`+vx>c|aQB++$bn@z?&UPD)KRL->-)XGY0P>7To0$62W z>rKOo`mwx0OM?t$ccoYTrN}jsW9(Iw)nO2X@sE*{18c{bOV(sAgmA`#unvn&qDGn( z-e0Qiksb=Ivcf-N=TdlO8i+H*;Ew_Hq0y@*^>SrxU&G>tj=8)PxSQ2v7dO||>G-ZI zk2%}lc@7M78{VmpKJq8Wr)KhH^X)yls*Mk{LGbM-=kwIdNv_u;YJ!Cn%^hpnXjKv! zYn(7iqOrgck~RDk0wlk}f-;?9>kVd~X9Y4#R67`vK3wXw?OK(g$S|0Nf_i{n zBS7M_R$tr8uHqh}>YPy^aQA>Zr(#f-4L#gol#G=8emy^0jzNcyV%9D;0rYcO6f=m2 zI=e*trV~Q6FY&xJd$I(RrOmfvDXu}G7Ar3n$TpVU5bjwBm>j6-Gu+2H>612aj_J~T zCMvKO4y{3{-G;z_^^`CQ#VqeDtUBrs>DTjAYssK~Aq2D@JIF6xo+v!KJaJ1NSk8mr z(_GhU9V5sqIeP~0orRhei16+CG>@-*jlJbD!7;U+qdAt18mEegbkz1?5Nx5)w|r#s8bSONzS z|L`45l8<9xd#KvpB^PuMEC_%8)53_!V15YwnBzG{(YSD1sXe$3O;}+jkV!}gNKQyn z3-hZvYf(7ceyT*#M9^g!kaKhU(T&~0ttP!Q&MD+a805rHa}h2kE-aO?4H1hA+(yf) zBoJ0pA>)%zFaN5se3bG8sy@rLksoS!yXFspiZXMz4C`4u{me?!~8Q$vgk(FWgp-<^EGw1Edp zU>yb+;X9X#rwr1s8{;?lN{s0;LCZqnysLJv7PYi~+`$@M;>VC~o5e%s#UZhXV2U)n zR5W8)4GLutnQ(5@tkmXUY_2h?N1*XwU?F3}m7LMJxbGPJulL*Ic}${+x0Y0!Ds`Bn z`#$l~a4nB#(AD!3@ERE}5PbCRH$JM0YXDDZ zhZWe~^nIPBwirg}SBI$F!uHTsnl z1E>);Fr9G7_f8$A z>zM>yQr%-wNKi^ZC}HmOklZz8GKvWstBho3Fl({IU|JW%7>`WKL!Pd=T=db_g+EgI zLvKZk;%Px!@+bHy^PiNId}GhIms7G|jyJzNW%cKT2KB|Oye~}V_yQjRJ*Mnuf;pOD zVCbM!0?T-ny7jL~oB;?R$QT7Z?p35YM{o9_>}i3IiX_B=2S!W)7L-(ryEA)ujS<$C z*5=-*TNZ0HCpXj}yRGg-Le3|qMCoEdZ^Q(c5&3Pld{uhW8t{q(*iEN30wHtRxp zqHi%({n9VXPH|ko4m1H&Nm;qk@=)TYRqDtXQLZ3?S<@v3lj5p^q0F+-DM(lQyznPZ zm*C84Yn<^-P76Gk*1&86uoS83z1Yhl0?SS! z)bv`Ffxalf6W3~b_xDTY#p8N_@blxy;j0mLHcD7bf!K^=LWRbWB1%GazI+`lfrXXt zQ?`m6{IbOqbbe}{`W$iTtzDGx4#Z3(tLx0oeSHsS9hv~-NCnyKQi*>vvUkFz;tgba zgi0UbbXRyr9JL(0k(U}bo(iSFf7C%*7K4^BihwS`QX(Fq{mwuO+(1ey%sj;*XHU&N z1Nu?E;&{ILU#_l&BVGbFQ`|5mo`tv1NY7^ILenXjGjx9d*%E0*M-mEgXIV*v|44!V zkPkQ>fRF>~Iy@4P`-g4p!VeMt5B^w0RGk|={guf=oP^|}qnvx_p|^(k4=j+3+aX*I z*S_V+iBj*a$wZvn`A0xQh&9tuUa=GB3fVdLqWZ^?qlj~!`o)!|8fm-uh{Ny?*Z2?g z^A81j{0eyg1Fj6+vT-=PCRs!bbp%g;qYr_JR@6(qbnq?aL1D0Z(+~N95_3M*ynoh@ zW`PyN{COm^$xXO3UqfhY@GrG5mFU`=l033ynVBmJCas$+??;N33j@~0s*xsv?Y2-j zf(A9YeLvdj(OlI?vfE15##7x~-v>oq%#&Hn+fG^o(O?ZRC+YFYL(YyH5`XOQ0u(T& zTnYI|k#XZBfHo6az9KdgO6-)BUZ!`E);Ihtr2N)UU8ElvQ$VRXrEiKu)8nD>IMo|G9BFMCiWr|`(#r$Q{`>=h^5%e! zs6zJVs@%nOzY_8*f3`RrxfUmfCVg_eJKpN2rYTTQ+abn0+JG^3f;%Ys|IMCtzXm=Fczkrx+}yT_RwE3^Y1vJX>X2 z;Nark3+p$1XUVGEwy~RC>#wjcgbM`LT*PR#<2hBqzkx59u>Jvo$KD;s#;WtXu;oA* zYFKD{Pd4aT=d;{OvJgQ^;#)3Q3P36k&h^1jHAfUH8=07&oSj_7&j6S%s2(fxG$K>A z_8lch1b{SjBcsxrys)d}(Z0+0i&qmme)wMW5jvi_vt|d;;%jf3x1fld9~2ltoBr}|t;fK- z%LC&rs_IQLf}Z`-rll~X9Mr!QPP{s&Va zXBm+bzk8fA6Oc9fg|cXE*Y0%K@%B6W?VhW%iy;$eNWeSdlZM_CEd#Ok+_JLoi67o* z&KZXZr&4M(@XgtB4NFB1UIdCkyiQsloU#q(V^fxMtQi=D!;YpSAP)$}{r5)8oEVVSY9}6x z4{Y$bRBpbM`&xWfZ*N#H(0ldaf{u_i4SPIab>= zaXPl~Igx&i*M2#qJ$N%fn4i@3x2F*sTw9Q-jj;VK?Ld}5GSM5lxlm~caD}p z*k63@`z-IB8wB%kA@;QKV(-7Qo2TW4N;qGhXylNte(VjN+2{(mD?$o3+My@$)EDT|a-IPf&W zz@!EdOCc&pFPZnfzhUM#mF)VZkyj^3tk89T1;8Zfhim487*f7rnz(b(fY?Q%l@E3< z!*_~RI!Ws*_8(kKdr9O7R@S1yE(gL$DKBGlj(_NGHw60832Ces6O8ZkO?OIaR#* zQ3%A}K2Rr6l+v95)_aI_5CoT{C(y$>h|xp*8aJxK=wX%fnUZe)=)iQ-nbVtm0uY5R zVPVJnrXpa%b8am8w`?=IjF@^n{K(PMQJI$->9e*!%aEP9*|K!hhlO7krh!D-R+4kBA$eyFy1#vpQoZM6|VZ_>p-V z#_RB+hYX$|-__{?0 z9l}m9<_ix5O#S0PWXx`pr_R!t_=$4bvQoTMXD5Gtc_#m6QO?sNXuO=6E1*&}3Qm3< zpE_IlB|>X*Y-gaa^1Fun%ldGidbbXwlL6C-#|3dLd)0~gAdIJ_Ge(tf z?!Yf)RZ_h2i~Rf;td7vt`L)%B;I6u~>P2dSjXp@tko96F^C?sK%m$$X<^I@wSM;P* zUa7C>55?{Mx5Se8#^h2Tx4T37eye#;arhyM7!ta60>v#vDB$2gP7><}E;Q$o-5Zq} z0CHu_v19QC;eT*}j~|b;K3M>`)8K5}cu1#fs!lPL1A0AI9SlFK7to~{H#*G0QXQrU zhXphp3MS=T;BQ>iDxp&+_~Q~5@Jnno`H=JJ$3V&C>b)v}gcST0f^P@Yd4OhmdDu{e zZ}L8?VB`VIRf(AOFd4MuOD9bVOo#&Biv65ae;>>cARs1b|6#gqrSv*DL0BDJ7Ti>O zV|OkQrPo!Eij2(lnitBE+!#Gfee1pFHfm-Uok7uTT->nirQ~G-P+Yf(8dVT_r@vAu zilF>0TgZ1~{CTv(4EneG!^dKy^J#_UH{UoQxkT>*!aFR?B~d@B1@IkP!*Fx?BeyJz z;y^6~zIQt5(H}-hRH3@gX7M4D!CA3dI`JTKArIXFN@0L)vzc~mv+9z?Ou>@ zC&{~X)qzfotFH<;8=(@CD9XRlvCM-A3($ApvJ14AWEulDCGYlMfrC_5&ofS%@rx>$ zhsvas&CfXx-gEoDMM)zmUgW!nyn?Z6W%A+6mB`CcJW=Z>-9%#hP|N7q002;kn%u_S z1|2P39#g(gSE$tNaM(5rdsVQ9VVfhGTT+s56$)#fsY6#A#BQtKy#LN_KCg8E7}*6R zqqN`ewAXg?$4r~2>UwMhWFG#=Bm7Vm>|BK0bJ83riBi=P2SQ$aSTJ$lX2YP)iV+8? zLSjENT#I3>xw%bQze|L4Qb_gq>=T@gzvp#M_zf<+|hjfvC1}FN3x^HC@!*#I7NWQigADGZ#qOaxI zc#Y#Z&bPiPr=VdHOxfW}6U)ckjadLH;!9EnB252`gPu`fMrB9YI-?k;3OHcDxS5CR zmyt2Bb+5_j+G>NCSv&IDeBPp)j==T%?6lF^J(g-|5M4Q&TWlxrAUA+2(HYP#;K8gs zFaN3jYqeB7=otv6Ku}X{EwahW06n0EV77j%CMWdaf{Tkd8BEr%_1klmuiWOvXGKYn zm6}@A^@o9Tg=$JCZ$4MO_s9@lvehJ=5gNyWuh?eNgo4Xtbe96Y&-Z3As#C^&tm;?v zkYo>#kp5+nR3UaG0NA>$ z1aIe74kp(3VHZ)Cba2=8XftjNRlLR2WgE;r@<5f%A3SWbJ!^jOlPyw2zp-MW-F1}7@U2ad1f`GO8mP*A*fW5Dc!IZ34iGT zjYSlXP{bV|UR5|~E3PJ1VWd%MGugzfZ&FjlaMXg~dul|VYuz=!7{BBH-N98>MP;hs z?5xFCdC{Cx@hrc8gK0g>2A zlD~tH=5(kAB6hLAEuyz0 zR*FC2d~nOmI=BwRA4UD$GxV-KMp1E@I_v^tWF3-o5e?#xI^cxhzn7Q}SqJ1_Fq>r| ziaVPzyz zjxob_f)54C>;?+8FK3xjfxaAy>;pi;5%#JDe*7XyTpo&NIG#ZJ4^L>ogN@e^Xj4I0 z?WI3NL&TK6o25=JAjY5ZGUvum;dJaPI!Jk5f~ok5?x$#qM77cy7TC-l zE21k#TGi$9 zxCHHsgcvp6BnZJ8bY7f+9h@Nl7G7|W4P2=iYzANAt8Xl9j;>Vy4@ww~1d(en{3}w{ z6eVzuLywYhJkjyibvf%ja+7f3=4_6w9TY_Uv4T`aZOLJQ@l&}XGeOuKccOoyozw$XeS8sn4b;T(l67KW03lA}dRsx*4sFY2nQ<5d!>ER^W{ zwL1E5Y>3@0Zy_Ig_}gdv4|HwkHIrI0s)=_&RD<|jR|F`!Qw?e^iTVY7rX|yHtyclIt=B! z3*zt?>CDvPY$wcXX@QIF-r^&j9HNbb7Fe`#a2mJhZ4qvp4mA}vhjH>iX=$R$$*-)~ zsw$HMq9xnwzw_z8PAe1Q&5oAxbitwv@uf06L09OJr*{K$fH?hbOz<} zKz{A&xNRE3UjfQ#brAt+TH;`88t(uMT=do=Olkg0B&?qW7#akVEdeQ8 zLkpHn?mJ17D&$TzeeU|J+*VqF3&uFhPcc?^u`wZ?!2(ZL;G4CFA|OlKpzZnDknQ!Z zXWx}V^!HuNWJ4BxlIF@GtFxUV^u^7?Iyf(L1qc+2hj*YE24dE0saTEZL-g<~iV1wvi}MmGuz8Qq@JZD%2&EW}OFaf5ejS)nY_12&XVnkad5< zG#*4Pij0Vn8;pP`*B^3heih=|A>l?#Fvy4$+%-I!vB2l`r}>t7dBIDh`LD#`VJ7q3 zTVrVP(-?i4`S*k4L+>rlV$*NhbuIQrY&fsG->aJl&3E}2rB1FpHnr|ETBIx%GpQJF zxqLRJYA#0%wy|g{lh*`8I`2TR7&X>nL*itih0TR^VTDX*o!E;3i@gAGad)*hYb!MZ z85M7)xdc6LXCUXRXv^pQ4P9}Ug*%BnT|nvh#>u zb51~aXLWB?r4;`V(83=?&c)RoLm2Gqa#=Tt))@DL=|hD7=8S#KiBsu9RW@gQLN@kn37kTAmjidgNXi3#iD8CziTD zUpSLib;D0_p~6>hA|rl?632fc-KL{IPkp_}XtA(qHcv&%9rXpu(G841AL8dppgTSE zM>C!lbVs*EhlW1Eex!M18`FFo=1rzDQB86cnu1i_)dYL$21;zjA4c1mw_ZI5pO^5G znV{#_H1n|CGmOeNCQ)ifQTR)4j%Ktx25eTB9q^dEj<-H~%frpH5GzE(zm~Hb8k)&K zl3&0$%KnC{^7uN~D3~_$U1Ny-0h6tNh9Ilm^rX({+;P@AP5_3JSpiA&rp*c{_EeDX z;A(~dHc(eyR2y`(8#9xfUmAdqSsbs7>pD@&@?Gu}oLtVYZ9`6Qa`4%iRX-t0%?R)U zw3mDKO|TCkH*t;rjL0X&*E&y5EKZ#KZ5|Q#*1sh0M^nj9MCr10>>VlVTK>Bx&yDh9 zy_f3ix7nM2%pQY0N$b7O3quR0oqPL<9fgBqbxwR?Yvy9N^w|UN16^({&##HB8?AxEN}XM-X>;<`Z~; zes8d8D0j4f9Kt6j_a(f5(ehu{n80LkP5R|M6F7qn=7;Qt2?&=rJ|qol7F{-}MNGkIam2IiZ8@bswWrL`&v(BK!4Njc&AnPahooYM_OrqW9<$+d@HFOB zLH^QS3J|tq{Cd-=G?9`0yFw_+q`sg$6*9O)aYOmFD|FpUz| z;X0@9YpnN2x9S1W6!3FrJ&=wH%-Sx{K- z^7vSy>#J>5u3i6@rX3^lwW7U z^QvzA3ukAwIA?UqGy#hyY>{=kqM>7#s2*VfnyO4x6`0{I`dWYZngs;@Zi?*N-NvO4 zAS}^FHqgdrKt?mXEL^#HoX#58ZbyU(!#ly1H^#mn{=R>DCgcb`2M#l-x9fP{!x84) zep>XnLDWx&?@*7wx{Z}?)P~SkyqlB%u9?fDWaYAzcKl}=$aHwy3wPwv=ntkeoP!|5 zt?pBASC(~S&wV%qk$X^!-1IQs%?kVybvHQl(8rW?2ZV~v<$K68hOM}YRn(6GIj^_P z8)*95l!K?E?+O_MKdg{HB4@`jEkrteo&_&AzZh!Cm>>y=JY^KFb5*vChs&lT#X_uBSRi!CT#YwCg2wWsOJgT?bjcB^6mnWK4&Pju*OR?bxip zft-Ix*mK16c6hKl#BRn8E>#mbM4%Y)D%xF0w@5`DfL6uqE=FR{e0V>hd$90D@*=mr zQWfkhel#?u#YsqOLOlTOr&crM``Uk%v#Yu-L*illL4jDH8L#te3&>>r?)`1 zfGjq-HM~a)46dzL!o0M}2EYH*IQ~P0pN#XL+G7b#y!J~{wPv}&D zA;U-0TdH*WSH@t(&QexOJo+~e=T~Ytn(e4_-Ng^MwyEUdyC_ncqx}bI3kWy)0_u(nUiK?#un$0 zY`QiB;Z1_gOv{t5oLhey)#v?=cCRLm%19wuqT6QC zRPDc<=g~g8!|fPD7z<{Xezu*j{$-Zuajg|l(<1if3Z2?`aRbspJr;p_V`g^>&~mA4x(0apX6M>b-KkOe(G|EhM3@!P5C&sEy=*@lWrxq$ZM z?bzUMl0@d_#G&N8xTi5k0MiRsHr8t_QAN9|O*mf#e#4Q#ogI;0uH))#~`cd9(^$6`O#dnwvCo1Zi{^D=RpZ|X9r1$gzRO*hpf!-FJrr`UyiqB=-a)8N>> zSt|;gIy+Oo=@y~a1!vlabP8F96O~_e2b4h5R>uQ7!(ti)%e3pLsdxP2DP3Rlk$tZ|@u$hMH=)zlj^KhXpct9V+#@-oMkO>@* zpHlMN^%PzxT7PrB8c-TICp1{G>KD&uyd<`9JIo09PH46+C9IPvNIhCyaV8TxxrXcy<*p(7N> z!MO!R(vMSeDFKyjy^0GDYi!KD%L7-BO!}Q@gx?6rbr&tvz;NknH#44IsZZ(Q%@vYC zIw|d2M>pNS-}<3mqQhru`#;+Q9@TQsy*EDL3cl)WU^)WvpVqjugdJ!DwQP|ReCM0% zlWDhjqqclCZPTO-+}{w#qH3WgI+IKX6N%{~uSm(rYKgnPmMmD#oe^}_q1+iXaw>nf zjNuxd>nr}A0Q0+7+WP!is-3&OpcnC{$HS|ZTQa@eJD2eD*z~z zIeQW)jh#C;@%Bw7eWT||)2y%Vv;ai|Wl> zFrj}&k`n7V`==kjZk2yoEr_NmJi*Sblg&R_K}P^gwaet3IB;fqW%iWaVEjhNr3rtV zE3MJyURVjpIb9&1Gyck;^6GQ7*r(Xbef0^G*5J3$;2rNeg&|a4*4XRc&OZwazHnGF z+bje5_x?!~UaX;%i+N$!RRqq(4OeYf11c%f zPNi$WiuwF>iLHM@bObJ&i_v>txK%4N&?c7yll4Eb_L7~3r&+#dBzQIOxVi~TeXrai zVr4}X7%+N4ubAIBy<|G#{uK~74F6a}?9lp#8-SzTfBzK))gk!dcR-k#<5Dcj?rt~Z zgnDB@(iDaH9+2}RjQXE9(CY)ewQi%n5sws!PNFax$NNXvhoNo9#q1Gx@2`B=R9W*W zgy1-RJLvQCD|nlXEA>A?`K#`h1Q-%u$wm#!r5t{7&Wxx*jLT1aVRqL^~}B za?bXoC3D60$PjE<>(BK)=;?EP)TMb5^{u2R+ArZ$Fkc5`cOSQq?J|yj=lhAUOzo>% zF3C`h#*>0sCD9x(1MxeqV7iXx_9Ir2N&~1I`xZ8++MWl#WEAQdh9vQcoV`p889myKc!xCPi2}Jfx8nU`$7P%GGD|i8xj5i9uFcU z5ul(VOT;X!Rx2t`GAlGx$#*GH+a&M@r|bZ0>;bQq^ClosD+i0QZq1DDZZhT3HM}-3k6vo=kI;qmUP~gv!Xkqkl z`olnq@I2L0B2~3)P$MTS%rd<|E-mG+xtr5cEf=YwqDYVkuQPf&^8u7|JXqpxjL*W{ zbq~wy*(;<{`74J=0cVFMkt$PyHMC=E;d!slVmzv2H!2S6F zG;MQor|e9`$90Rhe>v~wZ?w`naKKrb%VVX0xfjZk(k052^>}zum24l#HviBCTBm|w zv8Sqcfvmt;3*uqA=t!GdlS-V8dC~;$T|e3_e1&v5V+~b&E+67z3hL5eZ$ND_yOdJo z>|`7u#A#p?&?=%&*CgrDT{5D}H={B?$h`K2ny0|;;Wc6Tg7@|FuHJ}}k|n2=yz&_J zp)^{r_GzfNDFc2=)Y(?CGySC3Oj!^83DWAPsl%R?SI#u!ETuC}Wahl6m?ky(QfuS2 zaX|P12F%jKRaE0(z+vQ*jdg4c9s>R0txw5l0;b@?7hK&NvtJ!d`L*uL!s}LhKxc3qYCT@Y2vLiZ|E!4XXii-zMR?cCV6Y~Z`iZ!j z!0uuYISU&=`a~Ribfh%0roas@8Y2?&63R6H=@3O0J4?AV$ z#>mpjs#HkhA=ck}kA6ch+&2KyIuz~Ca7JEeLuG_bvOGecg#^jDe947URe@|N&-R;e zwLO{SJLcI#OotA3qEbCLkunQkp?XtB@^R_R*o?OpM5n>TjtU? z<+85ox*P6A@rDOu|0!0{dklmD4hB91_R~AL?+Emm?t8mDsGusUgl;yXz>=iHp+j-7 z{rQ`a)Nx=}0Z;$%Ro>uvuN0aCN28&0a(UG(^u(s)Bo+}p5xVKm z;lQ{?p8!%!BBN#si{up*Wj|^#WuSk5(~CB^q5EPfFZKIOVts!AtRw9N8w-zp^Ytu{ z+pGy~gL}}9a7j5uy)KwD==O5rRV82PVwZ99GbP_9)fZ1B16pZ}fH(trRV6R;4~f=L zJ@B!VP3K$_CP%A~``!LzqP5>Z^X>gkzj;M8;$2gV470!G`l+gFy`oOkgbBD+8K(rm$ z!19rq^y!24^fGxRkz0KGIJ*?yV+#x%v(45YVxTHE_$u@Ds0 z+$ww~xIj39$_DvUk1%YQnvTXHXPs{f97(wd6NJy_PuSzmjiQG+o zOx#y75Xnp^i6_RB2V<@+W!7tOqYR~Jz(-cIDxYM;!$)2T4OKYEvuR5+qg~^e;>2g# zeeFH`N{)_NqksN(-1m-h%YnV>TX)7@#{EN4AiFNNN+K25ECFO23sSTI6ryF83a#e7 z3yuomTsq1A`xhQbg_{~y$N5kC{hj+BO>UjF^$@wE0o%t#g>S^w?DisNrDb}lf4Q+$ zOhBh%jTngO83W3rmRi_6djdHZeEwGX0IY)oIh8g0a@u6dW&%79TX!Z%Jhnh-YqPTf zmAPLgQZ$YKJ=zx?+f*GRBPUcfmo&8Zb$i|osn{5PH-A>-mduA#>Dxy#gTHo8OgG`Q zeYJid#7=kG@B|aH3>JBYR(-3~@X9kx@y=(NDzi`oz zaoH$UD&=H|0oICvjsQ6#ngVlqO^nj}6{Y0|A~~+NG;-f&x0{P*WUrV{5vj@_?}xOt zxvu(wW`NV7`}dZ~1s-tY6e-xgB*sq%As|gn6r5!N>yHJo*Hu64#60slu~TDb-8>#eTQZR?(+^OE-f?RQIwkP?_>isMp{>Jd75(5Hj7lY#7!^ zHNWj9oikNqKl;~41ZNF1Kn$p*AIRgxSB1LwOHo((A%fR}XzM3ddgZ$FX9!@ul_RFgsY^UUw>W=yX(ZH zbONHzZphG^W%j%WW^!mlSudxLs>hUC3CDMZ>?n|8_Q{D4c$X4-#oZgy#er$T2yK%t zLk@9Y*9-NjBUJe`2opp8^@91C?jHxb8gA6YtF;jD7k=Q!XAY(Lmq78~913s@d`Un7 zKL5$S-f+i7re!5&S&S**`a_JckK@!1pBpY2!In`DUf=yUZp*{piz^LtFRui2R~po@ zi&jq9ATZC?=a^*wWK4YSh7Fzt#11i1n%f5?_(Ujr$p?oFIUl+jFrYQ#XKA9Y`J}wTUK-lt)XOYgA>@mV(!;c`M3?Wv{mnPT>)MgdDTSuC}!yq4HP_@dP z76;LfcPAxHzV)hKI>DC@-w`=i_)%#2)sk-*=96ximRiBl9G_}_TmQ+@-vt9X&>7Iw z>w5&u%*=EKi0=b~Y{lv+{_X;WT3g}Ahl`*|NvmQ>g>?RodL*}uwQoWEoo^jU&RpUlFUhx;Pwj!!HzQ)nJ zpMVu{OcOQrLh!#KfcLR}|2oaeR|3%~XC5?q&=E)3o5+#>b+yf%{VEa>@r^9;8|hnZ zdQEpo-zLWP%=-O-%-VZ%1qWWLcnK7SYfKrGh=Q(q;)pLEg}l=~>0@A&-y!j)CAqRc zmw6ctH3H_{c7UZC^7)xuX~PNdoob+yF}4?W@a=Y518pfS>ve{Hb~D&F%TO|)Gu#YU z=tU7Iz?Q`oli({xvK+iHOy+tw^+GKr>mYZ;fhc#aZ}SQr@vHCA-*mNrHTLVw>PEg9 z(%+U()CR4ATt9|OXxO+~v_;f=_ik&Xp3dlBuRD0O2pTwJBNyZ%~_hS62`r z=~JKvJF)t^?jK`r`{!(-QgX~ay=b`E8GrHK=%dHRZYbivCYu}9ZTsnA(*~MjxIfQ!6ZGC1va_M%|@RWba0pBiFui4W9 zAog!0tZATNhCfKseIj5R*Musz$`YPIx&%CQ&Oo+=>tJw))GpD@r^8fn^!oEB8%uo$ zBNk*jNsjMJP_uey`}$crPb+GZU>oED*9jaA0~~HyGFJ%+#M8L+KJ*0lL!hyD#_{ zXtqJtpS^Q3IMy#|)8>%Ot7>`IQzLqL)8;yBt0Rv6FwA;I&UdM}^931Eq{=#|W7z1L z$w5I$39Fmu8uj=iAKKsKecq=*-J_xBMgUl#7V_-9opkS)Gnbn;;Y`-ddOA2@2Wj5B z6^WWaGtl#XEp&kA*2l-kPWx*{c~23Efq---! zrh-I&WohW-FJfXh+YxF@mpF`PnB;zIEn>VHftDs#;;U;>@7Iro~&NOgv;xY7tKr5^t|%i zomr5=?>YO;sCOE);g2U00w#W=saolmr;&aqfI&olnQNN-@>(=QI5jo!u=;!`^$C^$Nr3oOsRVq|ZziKybzH-k zQD%C42#$`go!dB@jNxx~GQ~gH1-#~@?r^DwwwK3AD(q}i80xTI&M$l4Sa|T{CbQ$i z#Me6}?qn0vq|BNAa`tj7wof2`K#%vK$ZD?>Gw99-!(-h33@P#vGREAZAO<01P!`#H znMFBZAeIjgTwN!z0DP+$KXsH1GX=JSe)>7TjezbkWEd^yHV)t^7!%U!G|-5O^u^9` zg6bpBN%cYq2HYYamiym_pjf!rf7VvLi{-=XDF}z57VOM~&jc3;61KJL2nl{?1`7Rk z`x=T83#s-o1zF*>BcAhe@`^D>HNT{AbvXVA>-d9w=Jgr}i~$uR)ErgZw7|YkIp7>Y z#-e5t6(^-*K?F(L!VLrmew_xq!!=KC3Z$MxYLv#iv}ku$gR8QCuMVsZ^qJrIGQN56 zP}TbQ#n|5gH45wNw>Z&RQM;3luIx*X{P}L$@ey>PT#F1Ht^1Znxz(EZ#pY0bD$`mKdYC;+I+UZc;>9GYE zMrh4|xSOrSH$S_Y6^5HIIaQIWiRDd=l;2|paE1y7QpKC?#5j&iH`UK~#x_?5c294U zvq-*^mNCY9`Us^pxS!kQV?Xv1f3~^Nkrxmf-@2p$CA>M4JSnfDW>dfgU!kaw~@<@)u+49@2WSD81Wct`2+7_F~Nmr6+x!$bT{WxYmqSSQIx#^f&N?6L0-qW#`_2 z)GW;P$F9pit4oi#B5>sXb`5;5H-M=E(roNKzqnBXPpOcL%fLg6WNFg9qB898sRf=j ziNMzDWSZ7hg@C!x|6&IFDPLyR3JOcak4XP7hR{H5F>iXG0C$PjD>SkK)!YvHA$FSjr(T*A2mk~QvNB)YwX}Ik0}STad5l7hrO4RHeWwA zQB;1s5q;Rk*QefCM$5Eq=@N(DmZ8qh$!F7Eh&5vPYq`mW)#$Vl8DfGE80EYwrS^Y( zx8WiP55Ro`-uHh~T{O}Fo+7QxZadvDYzKpKgu#FJb+Un>gm*^t00xtjdc zQO`{>#PiwDn+`-YBEGc8-8k!cvf|Uw(41kGBv|3XEN3)623D8=|8&{iOn%s)z8dXr zjQoqW=A%(D##B2RfZvaZhQy$n#UGQ!@X{is}kCrkPV*F^c z6v7y1Ml&YJv2~6vzh#-jqb^#_4Yh!4q0`cIENi*#UMyuij@AB%giM@k%PV0ShRd?@ z2=FLdI8N8Z+jO0}-B@#97!55%PPyhikE%Up;qROuqxct6WiQa7@Bw9`f5>?MaP|(W z2YODg)%R~AS#nb?j2*w2Dp7H|;09CXe|YCS?ToW78_^ZXrbBXU>db+V9hn#Ns>|5T zTN;F290_ReGPs5Oqts9WyI}Or7tiB&cw`^=_tVackd8PI$y4FHM0kkjJGjNs-tFr4 z;FEsI?T_lF@+z|WOxZ$^Qc`=WB8mqtFf3-Uth2#? zEU5PxW8yB2lxp%QpPAx~lNq?}^|(V1zRWBGMrLWb${x{2cT6Mvn?E~BBVg@T!|-|j z^mcGNqC9KX#qB-L$j=dLssT;y(b67lMIwdv$>2_cGPOAgPAgdFv}2v<>gT>aEypi{ zYGH>mxVkpcdxw7kTOs1}G7?CXs@*r@xpFg*EzREBhrBXI;!qE-*6lAY7BnzB5R*+k zkb)Ymq?qRwO(!#`9n)Rf`6@8B6FlvF=^sV#{OH3CO`wD7G+$&Nf^qe|Zay5~XC`^U z)K;f-9``zq{vFTPeZQaZ5ly5O8l-l=AIN_6>Rb8y-hYx7?b!3Ws?}E7ND;V>-1Xs2d(^zC`eFKVxUQ)Kg#94@mf4tcMlMcqTHez#mZ(4|f! zw_3nrGcs#10aKAEXmB@jx#p~L!_poK8d?w~j1du|NgNwqRf)>l$@{&eZrQ3kQfc*= zpVJf{WGK-@CL#)nx4{d(pw0JP?5_*nPDyHeG?0bmw5Q6V@o<B0o7iE{qM-lCIjLB8=X*wXj;RXi}3J0XN%;>lW-;r7C^D@%jkqB7aG? zt)9oAQFCqk%H!J0bld8QCL41WVS%f>1ebEE&9N z<#vC~``?*QbJ`cf^NxGnntFm?0ks_U9|e8NRIX+#IDW^Qcz>}D_f0tCofuy(JQuU4 z7t9T?(CaQEf!IqP_-P7a^Di2|zTLLzmr*&J5DeO!Ev$RB)wu{R?v2AEipm9h=5C18 zuD&ljai$`=!Z+NFTziFBH^R&@g@4muM|%lhS#c4EgAv)M3}~O|P@$(x=Dv(8Ky2)q zO(Zj6<9Cs^uPxO@E;e5g|G^BU%vCD7_eaoRAbhTftDF9`fnn;UOx5h|HA zBHRTgAfC8?knH+sF4Wgvi7dS&Bwz02^zYOWgXQfwX-@k=_yik_R^akmW#ArB`KzUf zca&qfP(H#!ER@R11?zCIl2B3VI_9MM$D<-M5S9{)I7UqA~c_9v+y3t_v*#%7BMN^ZE{h7n&^^^tY=-c>A=Pef8KtzmG|ot z(~;=BwhWIH`9S3`_Be_!|x zDjbPE&a-1QEwIjOv}UJ8c%cHlJ>w4%vVzCdb9_U-=M{KlqIiGG8#FuI2R5M|>tp)f zj${au^llHs|ny)ErVEHwx7puDUPn z_x^AqTDcz8usmL+Lnv?b^z`4altt!UP&w&Jy7aZDm@ZG|oLu7q{5VXmsyGNtnp!}OuX z(|{AB?;1D2w}+g2(?If0ES|@!nRrM8MM*M4*y&K0%F}TqQAPH(qzIhmO6&(x++5?G zuVmX-n?dV->_gj^URww!p{L1KBRcRkL;H;pQ3;Mk_(?%Il3(mkyzLB4T^MB1eYG{% z%}1)H=e9=LVTK8~VD7+EC~N+kp$Qoh>jP2eXasxU4BW-u+hIUiQ+K+8SaZe}OGyYo z6Ckyhqh|3`ep8V_rqY)`)B%cI(#@b~luljFIQPvV98!dBNer*Njj5x(>4g1|*apA} z{YkfaElcEx0Kgl?&N&{W1Ecq6zL%1fL6hINLCB4HKi-v>@ zyuRy!VbRYkG}t`3fU$zzBQ&c}*+-1Dt~}{dq(CRt1i#m+ivHI<#~9GPCqcC#?Rh^} zf!c($;&};~G_*#vQQF@+M$S?LilMk=Ulv(N~5v{1*rdFS~LzQK);U~qj_t5;z#U9Ljs zwvOCD+SJ5Ow-LC3SlGvqS!5Z{V_D5 zj02nmL)`H_T79B5oHh03c8GF;M5|veiKey@=dT@qP#l3=1xCXCB7^>TR6{y3O7|13 z)z-V)=`dJn1s-ynU&x3I%u}Uwrs^=s;Rl6fV9&M1WmM%rY%Ib_YJ0|`72WYig=|G{ zs5+7AEyATq#UY7{d9^FB!6(ZbqMKLAL(I}2Dt#FF5iI|6?67mS?r6~{c7)bifW-8d zAMFTObIJLE9Ij_((O4a1rvu2c1yEN8!1Z~C34A(GA(&7e1VI-^X~^FEoF+hk(p8`m z_WR$wsM;djf(KxeeJf8=o8tYcY>VK05qt&I8TrhfP-dlo6^Od4N!cIXJPx1dVDM8q zsJazU*4c!E|1Wtc#|$cD$zuulpW0Qz-Nr1QJxtcV%}OahH~r_=Qq&yA^@eZ``H6Q^oOt!${wEE8k6*b2Z%J7O<5}I>tZSU|m z1Q%1Md$v+EV=L?5W@SaD9Jy_gi_E zJk+U9-2{EHEspYAZI4pg@CO&o-kYA_=HH8nMz+Oa^;lRu3UZ8?g4#f(l0Q)J1zkq4^@?o`hV7q(PBxmr|>JCK*DY7i;j zWcwb9s2jN zzgfp6u^NS^YU_Qbg=477${O(8w#|{@?&rrU_kl-4hXkUo*3{^Y{}PBz|>*RQ(UHCimkgTos} z>7#K34DDtboQ!cgc|nf25+{aH+P)~O!kLiXM$+WDStU&p-93_F)2oc0tdZj-T=vsE zVke{a?*rr4j_D+Wb!Y)drF!uREi}M7wS?g9JU%;1fy&AM(5kTL8?LrW?l#TG5boAb zTpb>NzrVZ6(JUc^2Iwz;hQ+RoHtR$;dk}e3h1w4g!;c=@#h0SDvVuGhsI^ztJh*X* z4VFx`crrM&4U49a-TYH^ItsDey1EQM@ZMo(MM}vS1o=B&`KMzWfRa;_ENKn9agaZOiNRWD+xv&67k6HvmJ-$k@Aquv@ zE1s|b`-O(G#$U-JBu`V?U$`~)UHjGD{ELsBQR|06gvoOzWanoSIgEo?TW?3rKx#*F z)f?Y^-Yw%Z?4MeYlgIE}BNB;j&P!A^E@pWWS1E1i(uAf;5Ooy{fjMpRb;^jK_`oM1 z7Lq0GQ{{B_*VSd@fia9F3~<>;QIkz7*-j_suI6T>7cbtEGc8IoRlz{2()2P(f*jNk zXCE{*iTvX)Z~AlGlxCQ&eTR2Z67GMvH^zRXf*iapXns80A{gx`;&_Y$`|S|}-;C1; zmmflbHjscz3`43-bS4JUnt~=jH^x0uOed`Fx`RjGFFw(h!Zuxz*o2fvv zxun$dpNQ~V!B_8E)+VNH4-WE_J|3e=Ia>>(W95eO4M6ekIx7V)$EJRyr&Q;>!X}Mi5&?i$ znm9IXUnjT{BU&E1OENDe-bCz{ug7ENIN1_IF?**%I|)(hx8)Ld@cMs^ak7X+?9xMBg0 z3g_Jn2XuiSd7lQcX3b|!wG{0g-Vn5!irzq3JuqOf3p`wVrH1!c)?W4_BaV!se=c;) zci5*qHF4v}Vp`^m2CX!x0k<_*zD5m&mlbi45C$PkDGF~WI`X8Rc_f4zj=>8({qR25 zZ;VDLI=j_PR}GW;a1an`>JxuqXR$%Xmxt$6jdS~DWx#W6m)P#fu+@>a+XqY2GGVk( zYe{1nGkA!;`weQL>9!N^uFLx#rjTDxdjsb+{O)C6z7VU0%fEEcj)Zg?On&?&0L7qx z^A_Eg#yuY1bBMf+qN_kb7XY#I@cNqB>g?pj6##*al2!i_LrIss-OSb(i-)k#(j=hy z>_Ha^dI=m?zlmSPD;GmCM%rjdutpTob-&@#NyaCEg+#auld~Shuh`s^KgZ`Da%Z^Z z;(|A*ODcPE#xJ53ntfn4xH!>n&3fBxt=uAB6FcFkE$cQL%}Tfwwp_At908h)7-5pmSgjI4<_hb zFQj!n9VFtUW|8$^$k!T2o}WrmYNU^9Y*H+!uQcs87Xm@zJ+>X00FjC&MD=yglUj<| zUp434X{~sk5if>RrXnfU(D26~kxb-ZuKkQrtA(Dx8a_wI-4%t2#@)sfevLT^uRn-} zpfjI(SB$qFf;p1)CVWT3VirQ&Mfrf)bxdP@cHS>NFMDqAp)u`KC?viz`f_95d z_2O}oak<|x|YaF|Bhl?4EbyPsus%pa9{@Nc+AMldF4dxbI zkMp4YjLw^%%GoUPPzJ@Xa;QSoJ`lY?d9;J6;X-MIOkiCVxpHg8HJk`TVz#Ktd=h6wC^@nP!6b$*a*lCuXUumqDzFNkR)y!aP2$&`XS$rL8ags z)oaW*@&EJ%-xj&Zj~^?Z*TgTTOu}t>pzSz4^G41`lm30b`APThjtv4nel3QqHcwyw z{out_pcYOpn6x5=xeY=iSY37fl?@m|d9X1A4<6XCD3mW)VqB3;o$GVhT`%yz> zQqYM@BY)!uktT?3teS!JnAZ!x5AzMPHT9SpfLISNIrEX*fbmI8PD}AglH%zq zh#KP+!TSwfScgaqIvor8*Ou6ae!T+SA^yyTa7}o-AB#+^`m*1HuTb(8c81ORTxV5} zbQP+@*~SD;9^axAKdfrmN<+<;II_a!2?%tjEs!5Sos#)e^!%^3>7rafk&m|%sCU>)!QOoK0i}Z%KZrsD z&CiXIj^!4Y%WU!9GL4DfGCrgHpxGgMt>PtRzJCfPoOu&O%@9;Rlmx$F8<4y3kY##x zVgcwO=ER{Pqa$8bJ~^RBwSh8Gdx*E%@98n$X=KxTr1XX^c~*Y!s3H&B5b4hn@%-v@ zlG_{F?0$DVzR6tMleCcc8Kw%=(Os3;MX{jJnFh=~iEd41d!eB@!Adnon_v0{njq!B z#~YA^p-B`<;a6{1KCbv&7n|dD&W&(y>j+X@WGoCF?a1Os)}@~`=C<3mn*!V{i`+I@Y4lZQt4o?c4%EsvuogJMBcw983)$4 z?F+St<=&l4t`H}F!{zpV5?tHkV+lIW3|WLrPqVHi<67q`eI=Fo^w}pKzM5Fqn?j}c zxB5=PL09>?^GcJ0D+C`MS~D{!DXAPq36=)!h(AUK+SckVEJ`?S(M1?#R>dX1&BQGk z2j^kBA{c#~E@E|cD~mb?4LkslWx;5XyTSPB^h2`e59*gx=I-VGK*G}4vY53R^xAA~ zA+IaS*ZqUEoPz+i)9VNs2dj4JIOR)TnGdsBhu9YSvR7IF_~;p3N%bHC_ts7tV}R~~ z8qgO|o0ARby}5YPQEjW#lhr~b@um2k#9y=mKojx}-hQ><=lER=(6ETHdsCyGm-+(R z`#(4`HVaSrfO9Va02QFjv;a6Z0=P&E)X6du_W45>wPjLt-dqA>g~~JJ8551H>*5fg zidc&_#3$*U>+zdqenvif)RE8`p5X*jLu{ek>DDP{pPeHAzw9CZ-$d!>9{^AU z0HXz@2BNQeddfwgGYU@M$C*Xe{U6iF3-vg&^98m~qHEEgt(h%*o%x@Gp2gUE zj6rt%O}m=Ttxp;j-*F{# zoH5Pg^vi7|1={%9aH8kXAeN20ZJLz@W(Fzlf=76acp@4CY2@}{xbo>l72q&p>9<|u z7Ev`rZgCBS&ubiAgcMo9?gbZs@h>R7Ztp-Hv4hQ@SS7%#)yG zMk;#N+_5RA7x(*_$-=P{zs|9wxZk8SdT(HJlC)fqE04P!9!r|h96uQCLJmHy?3-si zz>R=zSE@o-M)KTU;k)1}DTnQ|KZ%7Dv&ptUMclQC7@a^;`FsNquV0UNP!Jhli%9dET{6p?7|iw84DoL$_xv9Xv}&;WaYQK9Ula(? zs&q;!$;R%1PtOBYxyxT5_lv!iOH%3k=eM4qth7*yW$d(cG$It+FZc0_1P37nvfma1 zq7Tj1(iL`)7Ltp~wS?(@!S0aMHeKnNBD|K65rnmle2=|bS=jH^0sed-t*_)Rb@2hV* zhigJI7=%EIMs#+eF$Ftd+*udaREc^q;KPv&Jxv@8RQM#$s_esDZRqWD(lJ=Ko&Jwn zMBcP|)u-}HE4$Kvv3(vI7m^I|>RfbrQVMTEY4)Vq{U!F%rD^3*eKA_zIx6mR&pO%I zOxE%LJGZg?BDyrw$y0cRcTvS`TbZ7`!gsaN*>!!)zq{h-ax&^Jvdh2w+ppKY>Q{YZ zUCZ{?{%jZh#=RcfYUASSlmpi8X@9z8Ula%$GvFn_*ZuPpPsr;BTLGo=?EHCVaJsg~ zoy{8WTwd%vT{8O&DQx}nhQMeGS!OjAsRKW)``gfw=_^bzRUD(@#%E*3_K!msp3`!q zceT*BmE=q$oj4`ns8?I3KjT$gErZo&@72%^8u z41nZS@!7u49I%@61BKrc&4J!Re>e@3j-o+HVQQlAYqg&)dQA@JGuAZuK@c{lOOBti zR*3!hK00z7-}_0KL_5@2FY=jxH+kNL6%t<9*{&g0B0>8Db%kfOyefAP=+T1pHioy) zP2^c;QnRP6IEY0imaHnJyB4hYN`Hfmq)e}9@KJ0#;fEqI${b2s`0rr8Z{vs3hvSDH z6UpHr-9%EZ;sLUX^Jf*`$D}~&7#Kf0sCNcl1$Ykdwymi@1Q7iN=iR*MmKZ;D8bjHm zAxNuC7~C2w&;y(r3-=Gv%(-eKXNcG@IwZcv;zIeBSDu76bmv#}E?7;gKU=Fcs=Eu? zy#5LSc&8DSBO{*P)MSmTJiLgwD{r9`Cb^_{5-+bWp;In#Ps4gJ@z}3`k7a+5TxfH` zSo9!xSXvde5XjYhRTig6PQPFP@`wBZ9Fm>g9yeZ;gaEhCnjFn}zu1x;9$ZSH(w54) z2kdZ)?oc>xUN~Q>jV9dUfRFdCgYB!CoWqNd@hbb`ZQF|56AIY)989gaYX36@B3kZ3 zXP#%T+55^o9BJdy>{Fts9)i*yBDs(CA;obqKk+YY;j*-IJFZQ_7@Ty$Lz~jFDeo<; zXGH1`@o4y%QCJFU_|HJlkR2hw&rkqghjI9DPyaAu#A-^FxNlW1rc|@=H7a@mNWUGh zi%!T3QK4bMVneZCN0u$23v}Y5p3c*vD(oNLGB)Fqqkl2vq;ZS!n1&oon6~&s0!)It zy|pviQZE;O{_DxHC>2q_DSRWKmc}6Ws8Q}sjqi!M(0ZAp74KW9s)Yomgd99ar^te3 zjM@h3%7gKnvRZQOHD@&z#iFPOUqSXM|Cf~hO4+wiQt&GnK@>BRa5brExB<7z5>J#( zSlUsPcOC?)1uwq!q}yVj5%Fqv+ZHIy>;#*zZB212x1&;yta^_%2*0&**RdI?3Phqc_VwRXKPZzD~^1!_<`B}aLdc+ya!(iZ0# zus35o#GJ>joWILWBSoc=P}T%qPwkyu^$~a@^;yD;x5h?c*0GXyA(G3m`wUtR4tiN% zVqbzAk2aSrVwf)wS}!zOnp={4=R=?IrlqEW9HX=6i)eiQje>%FApY;5JEpD-9FBLVVgCwXGDJ zLyB9gP!_D`?TEIP1R#<2vdaH~M8jaO%m6PP8%i^h_#j7mmD(24(1KC2G4G3IK`9_YxQtNi0jZGl0io01`FRNIgTMPx$U^fHlo> zXLX=y`0IJ3?U*5jCSvb6zmdQmG>S1gxCLB**JdRy;WZulZ9rvD{P*8#z~e6EODbW{ zv%gX;J3i(&Q?D${uZbymaSC3%*< zg7lYMB-F9d=CJW(7XbTjE)yq+E5(jutjpb^2cONmY`(=%7(6~-%lYo=A@~aYfn_6v2_W$c41y&^ z8=U{G6uzNoTKUpmgeF;R_W?e}zdgPaKSc5A-GtGXUlrvz=@7t{$?INo`w!y0=gpsm z$i+(`g3E6fQ-Nkt7O}IoD>LQ+2fQbHlE4d}hI6B6*t!#(Wmh2q^5+g}q4=%5!(FXJ z=R(5VMYs6{!38{@PgncO4p2gTSKvMo`w~uM{6vNA9r)rXSlbal3YI%yW(6A)m>$$+ zCzT4eV*0Gz7I&6cl`Av={tEE0;Ki%?dKAvB7S^}KkwPEv)eYm!$brhtG)c{dr0#eI zM2bv#{Etec`Z_l|OkZ*cmJ5Q*w9mCNpjK3;9gu#4_6#t@{<9*zzTf`RwLt#5WcB6B ziKqE~;G?eEKoKlguBLQu-<_^BPJYGvnpBED5If4E8g+i7%>ofSYFD&6|O9DL^d}UcO(o4{h1C&Z=5D zIO&!9dw!k5j&p>Q2noX~CtHAx=NTF#?T)(%ogz8wTQTkWQdaUY9i%b7Zuv zdX{Lg9rjO&r;tx7p3rKou~+x}C|*mNneUunk@d`+p}Q@cbtx(arM($Il!XJ*4ik`5 zGK5qR71&iIEd_Kvjv<8xnZ-aD;(raZFwPN$hX3=82Kwz;j)K67V#pmjX{pyv70-8; zA3MNxr4j?_2e;q8DjNjlcZ>xKHX9_6YmDq3Wo-8AbNzl14PlM1Tpv)FAc(<%8W@SEIXs;Q zUuK=TVk*2yA5Eov;p=c*vXUDlL|uF1j{^_A%+!O0&s$cnQNVj7tO`|9gg8d8d!Bm$!`<&9Lya*?S%lI} z^yRIH`$Mh;C0ux-uh`aq>Cf7V?We%Y4PSNy=9kKN%SWuFV`C^LaXx3~w`NS4?{A;Z zi|!WeQhibA%!I^|kW9)354ZtNoZ0wzf^ZAc#!U#guAsY9oA?+gsrkZkFun@fCc zuZa`YG%4{d0_lkWLr;9-A0F>OLKwP)VbpS`JLo>lE+LMw68G=&d!yxgHcHxB!g?WV z*A;?J`|w+K?agiZ6^x!Vxq-QQ)wviFbE0FefB6*s^n>EBIhO>ucdBF z5Jn1J{8e_44zNUxd%+67r?qwYToOl}%?&>&;rrju;&gVBoPx^xR3OZp%h4P{Qia_2ic z>_MYDNAt;Kd9mAxiF28lt)X(pn{R%DMenaQgpv!j6sT?CfZ`~(9tdNf=pBr_&20=%<1ezS7K;DtHv{j)&hQgx3ypW zXQ64S>phXi#Kx-9My5>Nte!>tnfdmXI6{>@rWkSn?a{>UMq!aFp+=r~^gnesMy~3> zP}-1$7&*D$gYJTyH{B(ZQsRoRagRG?E02%ES8g6gA8owA7#V{4(H)TloOd;jxs>GY zma=m#!Q$sp#3Fx{La2&d_ZN4zYOxcd!@^SATZjEa2Rt<zeciqoT_pGHUZ<(V}3} zDHNqQ@*gfP&SWv0oEi~_m{7j4&fmvi$(#c_3T9&xw32ync?{yMl$_~XBRN6`L{t?j5m9GlFTZhPL5oF9tYupbBGufw&;l&B6j_7m3u1MK z&;1wVyN!R&CaH;b6!qt(D1X9>EJ7_1^&AupOG>Zb!-f{OY|4xU7@)}!HH~A`SJ_PX z$`bMtw#Gd;XIa3M<55SOmCcEbYD- zCB+3S(;H`*!{ChlF(Y1r=)(Hp<2-=R2*~bwu--Z2^jcxAs7V-tQoeD&cd^mi>5ycg z*)tvi-~SUSrOla~B=?=1Z81Qfc;mci-x;4>kD7*~do!Tc&MD22v<`U;>4p7wCjqvh z((eWInuO#j8G;bra#ypPHJtD0V4?&+b$h=Zr;73l(|8G~kNRa@WNnC3``KG`Oapw^ zcWJNLAn2$c%P{KkWB<$!#~G{Ouv(obc9=Is{^}*WCM%2)GfL;&e|+* zqjIYTkOd+k#eZl2fBM>Z-tX-JaQ`8OI9Sk0!Pmr?UQ+#2ITWwcc#?h15I56pg4HMc zL8}sV9>;2s#XIMzucr_=+hv$&GVPm-1EMiBRbQ*w9R>^|f^(bv$U~F}IJe}x$&DEc zi`vNQ67z5d*(JVm=6_T&@+N=<+;G@n*-G zyA9`%;Edyq=`X$L_yAz>;{O$xEW)`r`Hq7OyX+@Iov+D};!7=>OnIBlKz}O_Z(D8n z%7^+jQ;E$90TZ$k7yLuZTV;3ZzjI}zIDMD`R5C?_1$^-Uhfldjj?hUU>VSt5Jq~{p zUy>l-&_rG_jH37dtvLY!V*tVLf0<N3arS(oaZn$Oauz zMd`nv$$B3=D@V`7W7P*QUfZ%RIztg$+ z?zt-UACK|5=(OPqm5RanuO|^SZNfSHO#Z8}^?WAB4sN_3nr~!&;?9N&yu_X}P6;tf z$>i{Zb?b1!r85{l9OTqOAJyjWThD45YRW#16t5<4At$xQ0t`P?Yfh@ho^6%vHsIZj zY3la9#8=9`L+&wfasfoB?Z&#rAm!ccdCzAfKJuHNaLdp7vp^mMppv$M;=Kk3u;II|?-r9_T$ zqDjJSugd55yOw92MvAi*BYiI-52YWfi)>+F)H8prdq?_4&*v@+tLwR|i{`a?GFlzZ z&g0ADd*o&Oz^Rr`%8~Q!$A)ic(Uepq=_Y~Flv|Dw2YG_qN27?KziX|u4v@Z1w%zK( zp69=C3?zwb^uJXC%5XD-HFRP~{h#TX^jX^`{eQz3{}wAeiA1yr?blB6P7XQZMoWH_ zyDv5dPKCP@PdY8-{3g?(!sN7nNh-$k#YKw(!zwcyJ^bt9WVq z>?TyIFu`C!@Rvm7^nypn>yXGn%;}?}PoWxxJSLD_$ z>poUUr+8-(LGQD2fBUoeIW;XUpk*p@l%j`f&dzL;BvwWe{_WK!cN#IYz;F!bsMoq~ zMm=a8f!60Y*Zk#pv%8f#Ri6nB(Hj=6$G{#$u|eb)JG;FHm$fwf$U8ide-gt7a>)<& z8Iutv1n-vO)a*cszZL`{>YQ=jCjNmg48DFDUx@L}!IC>QaP)OsWrJqn*Gn1wXuCCp z)O_7csFnxi$8;*wkM>Q%W@XM>sTtfcJZ|DldJBNBHV_7+wxd=+V+ms7{Ha<%Sg;0m z50n;Yp&f>FF3}A`RxE5feB_ifh1)hJ=k~NlmWS^`<)A2jFGZc1gg5^jx7<~AS1k5( zsa;2_DC#_3-u?-YvYD?r5wpz(<)8i@9T9cexOhKA7cM_o$faI4&5Ya)`%U;Z2fHQ4t0o(^qS$sn;9h?+y@YFBl2lSRU_Q zytzj4^ABzn1=qb6`B?M#n1A>rW9D?xy!k1Cf1~kxJ0|6UC(q_lYEHv+x($mh=gi z5^cjsry<15^rs`fCp-O+dtdR?cF4ugqY1{FZ->o(xcQhsl(}(yRv`ll5-+L$ zp#McC8bI-%;f@4z!vf(E919!by`ILTVT8Z#bqx-0$n|7pRm8kkK>T zr9*w83z1A=4M3E66tYdN@+&2+LtNI7q0O?=aiaoSZ8e_T(jHh1NpjM;U>SkhF8F@k z&oP#zMxKV$#H&a+Xc}g*t`y%5!?hzwVKPKdqPlf>sl zkC;Os$s7`~Y<5Yr4hEM+GH+jAzG+0w#9`_UjzJyKatSu{I{1&kLN3C-Aa`>>v#!fB5vK(0|sbo z0`-o6JRPk5B)|Do?|oSp2NnA)=JD}$`_ss(4RY}w&2g0O0NJd~#wH#Lm_6Kt2p#WPtqC!F>3O;v+Ld*rAPzExS?K;r_T}C%X0kx;z`Nox!)(J?#o_r74Tu$aB8g9pDfW^5Oq)rIb2*15O+Wt&TfCM zj#qUN&)D2YaBgvJF5nc0$_J>kCrHZI32~vRR=x>){km+-Jh9wn=*JS0q1(K_7-F-p zb=94iKyFS0v2-tl;~mH|n}VvACh%Jx@iqF^i^74i z(x0{ohwYf|pxjANrRv^ueQrvehP!g>>SkQ7Woqi3>YpR(HN~Ln@mV?csnEc^SgiWB z>T(%?x-tc4T*e-yU{=Xw@*dVQVuWcux ztrhGLuDV25){&-#pcM(nmHze-xE!YZPSM#JeP+%K94+dqKVV#nNfiw7jiBxCAxyq0 zb0H?ARnNdUD|Q5ZRQvJB_{+?jF!fv{+4Hjr_A4^WbFHAktrsg#r^bl8vhr5s*a(5; za`asZrCVSI%i{MS?p%2lvUDp59fZSety0@5yAjEoO4Gg2ajc zKXsjTSX4p7_LY_;1s0?m1eR`)l9ooPr5jut>F$&+X%G3D)Nx9#B$_-2KQGe3j+2pVut|=?Y&hkO znc$jBcDVQ>vgfw~aB%axfi)sCU6=;ih;G&8+~0H~c@Oq=#dM*0;0Dp@TzQ?uDmUfCDyo11) zv5zAUXi?JyfFUw^CdkORm^2&fk{Bw-79@At)jyfMb-l9wR+f8ozH*!j$?SFsJ^RcziXact;p(jhh%L3y(h7D9SccV{3Cr- zK-+Q{DW6KfYEY(-gIcp6Bm~DG7a4}l?b^vt1TSG%sw!#uMKb%$aja4M9v1(pzQgPe7;&-@Ss5s@k3 zc*u<4Q7bd^y6i!YXAiixfsI)U{BY}<*IO)%Co>jGisMZpF*XEA%)~r9rs*l#l}_4M zk3;^IQPNql@ueX7ku%pt+*tTi81`!W+y#*-^N5zEceQh?1`@KS^-&%b{>piZ-f=LJ z{csgbG-u`mTmvr`Nm)jp!FP=Tw3^51P@cdG{#q2ISUKCx{f55&X!$p=6>%%3rijRK z+sBUMOdf07!LIS+8gn1vudJw+Y$iiu7Fgyey*6kHp#oIz#WG*Z7T*oP+Si=QJj5OS zAzAPw^+kj88!^#XumV3ZA--<;k&OEt&Jkm)Ik!?w1rz>wxn+12r z5|!_E73=Gr3H5?ImC0b(my>z4@!9#}Hrm&Y4nf)81J<<)^+g>L_!pTR2f8R&d$$0{a0A&5Gbbiq?7mLIMDAa zuU5Z%fXtyb5k!A&TAx;2vA~7elHFiD=sZnrv|suC%9KZyC-O;4^v6DYtu<8U(dTDD z#9x1$x4_6_Gi&-#QaLZAeYEy@dcoPd?q2pSifip?2?~lz#fd@pPpJu0^f#E?+S|r! zn-G~gB^QvzcPEt)@p|j>Kseb+CRbaDKb0G?^d@N(Xae#PiCu=jH7FcIE{M2asi5HV zHaR`~I{6#8BBt`rII$}|Mj4y#@M+)YTFe*H)ZygaG-6&eHm(2$%GF^^*J71tLHMrH zT(P><7EGDcz!hv#TR)kmiQDBM1+%-61;%-9JwbdgJ!~CJ)tWnwO|R@k5QYfX;sPC{ zK0Cn1RE(B<7klWKdnj~X7Wb8modvGM{Al9+5;~Km$l%PcOiP>ceN=cVa;EJ%2{Mc+ zWl}C-No7fOox`IZKIf>oo6ln>Y@!{uN_4$W6@b;b zOvKUZIx@BqHqTinp|iDM&623_C{VeetNH0|9>4LG;Z@{$OB&!MC0ICu3;;81Tgb?~ z`2>IdVM_h;XAK26ZuesFY2c5|TJW49{(vQ3nQ6$*D(}0n*({BgLd9!#Y>2)%u*bcp!`Tmk3`r;xgS~@RT{i{}{8{BbIuc#_nMPLxBj1`|c>6Z{ zFx&5L%D8@?%}7oo00z^&n2~Zqot8hz+~wHRfVJy;qP^?w#{|=}6^I{)NT+F9UTyY$X%D_shfkmwV}3(^2|R#%v^ypThp!U^RE22?|6+X)YxUZyN_d=!+pyg zfdp8M-PR`KR$aI`>?O8CkPKtxSS&!xzO^&asX?_N6DG(P9@cEckCEn}O|5>#n(BQUa(YcSw9ETJHB4p9 zyDeN&JSKZW{O6RoTb|SyPo|k5PUD5(tw+cd>NQ zSeFIG0(gdwpn8d;fI-6l0u2qQI?v9!^ELoCcHRDt)!xibiZX%})K82~ZggkTHH+uh z_n$G$SYN;AAXF?BNF!2kE*TSZSZw#q{>K+|1T|{hA6u9mE%(mOQeWAxJdBrxgLFCB z`F=mmceFM>nV1~x?cUbapcnK8TQx51Pzb-ulf^T_DLi+vH(7Xz7ad_qNUKK47uNl~ z>gHtKi=4g~?sI2<#iV2rM6ZnActo5z^Y%sBe@n9wGoY1hXZPX{UeMLzY? zuGRxjLU{r5~7gfVn=mO@mEc>#AO?n)y-VNM@5WM|0-^@=?X6! z@;0c2-#_d(gF#gw2{#m&W8s|>I1+L(=5`;>u-V%Vg6yZxAH!nmy2_=(x_Sl`$iM%h ze)BR`pYn3fZgNSq% zKQf96d_kNQeOE#40eDR-(!`brj;_V2>b3AiuJ~YBfHc!}$bAA=Y*=>^B~>PtU$vm0 zELFSBijBUQiKI`EWv=7Q?9cK=(-{H!!-gkG(9`^JLfQadDxn0~?wwN~@$(PNh3|J>ervrCPG;>3}x1aGnEkQ8y<;X7Pd`*^w4 z9YzIab;HI?YCC3n!7#6a;|ZxTCwI_W55ff$r!~C>uiUHQhc%LgJItDveeZYIC#vr1 zis88rs~exYRYKDqee3X<4fmty`4YwL@w+xjn641q4MoQ2$r}6$^O;rkimD_#)P`2(wB2^wsm5w3c3T5+q}P&%zUJL$0_+ z5PAt6O~WE|^MM59H@8~@6VV7pZ(_$yu{VDc$^Dv&k^8)lL4mcmJ~;$lhC@8NY2rmg zct1B7K@+?w&Iztdj`pR#V=`^f&Yzppto+r|q@AWfjb2;s0j`=gsOvQa3BYQv2NrLd zxzQ!;?lWJ1XJ!1`#fF9GNkEwcj~ESVTkK%_Wj)0x=wYgvWuT?}Ty>-oA z)H(Oi^e=L zOV^1ik((_^km_d7E3f_ET;CTx{{%9O2ITqUkdfX@nTr=F5kO&qm56ijwjvKb1zPTQ z#(+;;5{r`|=ojYo5_jc)*2!spNA{NSBinmvLaL-8_njP#joz0Aw;kw3P;Wm;l740W zCBagTsK&&|r2pdRL*1`#wU#z^+}_#8mI@g%1d4Wo7w_W}*!|>0BU$~1OmTn?;`&6q zT*;*6m!m}mSbJ||KAJmKZ3wXX-pc?~ic7YI-(#at9@@gkY5(=>jlWbB;7ErwApWUB zv1`?X9)Ul9W>CnoIa{08Jg6AkIuvaBk?&;s2CEF*53g{x?$3vp;MXy*qT;0Y+t>tFhe?trNeNW`~vKmjrm!ffc{LOCUfl|KnL`vEHkcEoN7FZT6+T3pl zhX5}(`-!_jwYT6uOX<|Qd$w%j+3i=|BTa=n&uM!N!{ltLBotOVz^eiRTuMk>5|zpw zb)UZwpVFzToyZs#hS`mha_B1mfvx;HRH5v4?;S_pe9h)V9-}3>c#L!tLct+DnkoN_ zf@Xk{q8hsmtdH)@(xka_f%Z+R3mhW>8OF*Ah} z3-rEx^`9aYvTbXx8_WnUWS+c1(+EobpD%P~13TFf!+1j4$}1})8v+eeA%Za*Q(To} z@k#QIi256y^K|S_eH%0yU!1Ur`uZ*9-^C;(_fboFX>o+0^FGZ|rp9HJ2a>-36Xb4P zX@{!@STG|n)^@)8g|O3ojXC#WnhRR31<|c6uE^pzb+vm%dpzw0)+fFGDa1rs61Pki z6a)EKNB(4bN#?#)IVVnlH0o@>mQREiu2uo!@yp3k3B&D{5EaD|$IeqKqLAgGZ1Hs# z$JZ^FF?WN`aLePE%DxWwM2SK}qv`HP^uuuosv;N{mg$!}&AefLVxnXu2(`|JZiD?- z!Ta;SJe@7wHpj0Kgeya>Pq=0w!@2|?F8zTfP=um(WN#Y318JT4S%W)8mtV}FaizR< zJC#vVnNlLQa|`RBL%wQVR>Avmi7jhUhV_S1>#tK&Tt7{X@UD+fCKhcwO!H1pr&65+ zebjrjw0W0 z>l}gm9UJTRCmA!@^Oi0M0c>8(F}&R#l*Jvl3377V1$VtZoOfqscRM|A2&d+vwVB`V zfJDQ)eUi2!7IIsiTffNHQn;0MaZ7s{D$5x1@H(MAGYA7swGp5(2nNPdl!$zLKi=aJ zY~5a~%Yd!F9DaOz(AePNo09SERSt^0IF2_KLcO#qD?C-oz{YI0NFXsPd|zfP1xN-&)OVY5MLRy1mS2I^6gkikpyY&DQ&4(mZ9esC zg?8srzbl7rcbxu~Y$*QkN#ouK1{&I0rX_MRnBCoYe&Chq#DC@OSD^HhXe5RMK~kTb zY3yrWVq#p3$p0P*ksi@pqW@!wPItixyKqZyL~vKw1yfRbl39we)N3btV;CAZX8;jX zQvM1I2?=>l5AgAvn&*%I?0^2q0a5{+1f=xe{?=K6xW#;5RN*m%!IE2-< z?EmyKM0Q^Sy$lmRcp3}iO3u5ve3>~;_cYGb^sdMx1d|=%<@Yg^IM_VdvRuk~dwerX zGaR`$#3Q*Hgz&a0Xi~Z3GI?!DF9nV_WTCO<%CyDl-MsNA#p#HELzLY6iR77B;~LP6 zsJTp@y~%Xl@1N)|KYMXhDpN|c9258gy|c%hwj`5J@WrC8?L5;bCG7Qx^Kd>1)Ot)` zaM8<=HuBWUIsuy}TTngXi~rh6{rgJ-vH02e>7NBuaV4~mK_Bh;O27?DgZ80a-(l}; zK;358#OQ3I%!`Z)2~4TJtZ0G<1c><0Fscob*uB}yVqMnycW)?eAQBUrTg8D!_#9yQ zv)`0$c*-c*YijUW=6s9^8llD@gdbHO{eNO8g)%vwUb|KcB2>5n%b~!!E%WZz>*N`2 zB=(~AH#w8aYB82N<$P(hApAw)tWI+#gIZgGw3=zG9ownwrsp(2-_L|~1z;-xSV*(a zrU^+a8laZ8y0>n>9=p`R2T`qEAb*nY1j4I)vIiCPUVZs{eh_@qXc1h+XFg{wLlQOu z@2!&ec`aDR)Alcn0*|#`*1Wcm4k>Rk#h4)%^VV;r;*-+S5Bpuq_pmqT6Iseg{mh5p zgW7e>fWA5a8PAaZ1|t|3x7(Bz_r}6ubcEYAjSxAvqCT){H#?_r?nkUl;UE0uE-&nX?3RV&MIkPOsw2tEoS*sakQei-KkK{G@1k^Db^ah{3PBt1mf0E*k#2rAb{7gF zHX!!DJO~+b%GRhHcicFw&cl!x4rEc61AjHWwJc$K1!zE*f99?^lH;a)BK$x+_ED-T zUr0K52jEr$@ON@e~Xl#Y*_?9p=`YF>p}wv8Y-xmI7r}pLMjl z^zR-c?#-CZE9*rw5K zcmsSdlNZoI_)$aqGN(|&C_JO(akVlm?LM4Fol(P9Sx$8V*XV@Ti@r(BA*pwO(tdSP z^Q6mM$Fp+oH|RNfzJu+G~|R(WpD8tv9E!OU}WOCi>bMd(MjOKT_sap_x*?ie#_ zRgvqPo!_*E4J*%3t(ThX5~az(y3AZqE8eKjnQKN3h!S*)W)-NEl@sytOZtzPKhe_G zws};5^>p2TX#63YU}CKpkG3Qmf1vl9+2u3EedTG6;6sjP7(Mrlc@d7ZgK6U!gpWQ+ z7xYRo2VlH9L&U{R`lA>_U~iF_qKVs2@YVW8&Ch@hnbejxuASI?)z%#_F4~1L0<^NPJSbzZB&tkIWt6`Y#Nu2c z22+b63CqXD*4NZX*tkn6Q^gqDPwbt2hEaH`efk3P05nAljVhOX(zY0Kzp}8rJT`qIGeBndN95+HR$tW`)}u$GALx3|Ag9|JbIB29 z!$t+w-~XK^x?JvBWfeYwJpRhRQ<69of|Xmr>+NxXk<hr;9B_M}qc}KZJzPM=6)hLxm;Z0#wtK8fnHp zj_G#pLV-`S7KV{pcg%t$!u% zi})Svk4So*Ht#pn92QKU+vcqchY!c*$iefo4&jQ9yf;&P&hpd={kB&u-0xDqv-L4! zWrR*x*YbYZY%Rdo)^^*R-Rh3&V;14~G@Em&O~q_mR!zD~Mu^1HwPp$`-x-A-$r^61 z?^G>$xGUc;gA_IDMn0N^yJMWF+`)@Bn%SE=>AT?iH{RRAHexb67yD$1JLxktHk#W) z7UZShBDe=bDhU9ZscMxFhl?@HaBizNnl)+#%vS=~j7P`~!{HX!_+10M3k->z^1vC7 zw#WJjvwV1dv=3$uj=nFzJvPESx(tL-ojCWO0Z7MzAB3BSM@q66&}56UZLZqAc$p$S zPUK|9UzKi(Rp8E57&I?Wt${W7qXTHLdsnT8dOY&kll}%Xn1s_Bf<%1dUreEkUG(-U zF7v=dwU2IAOrh?*$WmXi=U(-g9x4hybx{0qhVei>ob#af+M}BDgZ%T;3KNe;Gpm8{ z5ycLrI`ca1JRf1wHubZVpEcu!6Un%WNDr=!m)gNdAsT7yRbGh<6^Nn8!6Az4)cYCj zF>D>INO{PW7(FA+5FHSKq_(92sDduv><%fIjat~Zy@WNL*z|a^IMWdYNhgIGkCm9R zy$wLRs9>WAp)fJ607#eqR~6aFGh(oDB1iW&5z0+=8nB{1FPMjJ!AL@VRopN@*+8xn zR#8*a{h9F{_@g9Jkfc7G`+mX8GZyv9V2SUJDi7_fYDFn^`VSjQ1RB25&vpUvkZ*wy zRzQNX^kwIFj8S@kr2zB=_=v=s(PlnL4XqQrcO{Dq)q2Yr-5l*%Q0f&<=yALg%2K*? zubigsmORkMzxAtIn!vDUtFEeB7?Z5F#=EObKgiQ{7i~F*EaP^DQY#M9_wEI9mklhZ zhD6GG;V`mmHDShY7v9PCEy6!gf755v<70yBm9p2HF9Wm3>`>k#(2R;Hr$c5GIx|?;(hG`aZ!;ByEqb}WKYk+PC%N;gKI%6DTcnuk33rF zaTeD5%bbr4Z=v+S#)%EZO0@`HKP2NVuj;-5>7@4x7wn1_@w@{EL2NVbZvDRzww&cgIrqQ0DJ%Xt{vWz`BwDj zcM?MlXXzO@e|7|Tdd5-Tc8YI+>-Uwc*{cbOCG8iES<(iXA5uXI4$>SiU%|)AnenbG zkfi>4#SKvYsZ}gs3R0*6TonhDN^FC~_RRTx=OncuA$l`n`^5>!CVlyRmI24wUg-W8 z0MxWxlR8EYFb3y>1QQvB+q?SzmZ;E(e=(wl!OvroiQ>)T@kFZNn*8?MSmf1Lu!5vv zxk9!^{5KRnVU0+#PllijIsw)7rgaEGu`4K{Qo^AE(d{h#ibc02$iNIFhLRWU>8Rdv z!fhAm2d7{^hF~wFKAI;1=l@$e7xogz(b21?vKS~N42@z=?O?~S>gpD+<&8hrNpzTO zGqbA=4=ksHe7Kr7Txs`Uj|Q;t+|zhbwca*zAt4lZnq+l~W64HT@dX=it6EnStGOz< zi$4k)R6d`?L4EZv%-~8+Zm67)2yY|039kHfuVhJ+k2o}xBd#VyrdXyfKSi33*5Y;V zZM>;8w?3>&f}T>r0K^gW6CDvwZH>th=*jZV;(Yj}kTz*JPj?YZXbK`Nbo_<@FQ9yf zgtX-k5_fZ>_2CGnJ_ZIAHM(L3eWW+xMbY#l2vOmerCS>PC_g0V4vX0O#{E_|mm9g> zwi@>WvMA7trQql6KPLj4LRzR`3UY}4#@X$s)Mu9h#nw_My|T0J6c9q`#r!Bb_RpYd z;xHv#C0F?-7WYB$$M@SWKB+0yD%F~!D#zB(KfHSt`fc~HL9`Xm3i&pta2MYM@i#F; zS?HAn!0|Kz0b4j7{WqdMN!#o~<{o;DA3!$mZz{Qdc#iFPV*pYlJ1aITmIupLDKw5# znE8$WQ5WYZ{MoePd6T{HHVM~m$vRE^&zl_&`N&}Md>{X=%TuvpBo3vG-O=`v-yorh zEmY!6>^CL5OUz@RRk3*Q{|ldV!~RCk>Cn2sg8s$wwNVNIj!_3XzP5`&1e+v_+&Ov^ zJKGpO`Rn_7HEfOikUsV3sF$5=S$cat3hK2^I=yJcO}<;d_1SAhEA7Ib)}2LX!3uK| zmx`&@dhxswzjy`t| z?l?0Ri*V8yeCqYZnTOOO(Kgx6o4FhhUjF$`*JQEMrpoR0FHN129d4)SE zC*mr)MoHm?k>}!+d@1)@j@k_b&fl!Tf#$+B>wJw5JquHgL#%i920+R_8@wyp`AlRX z^GJ@bh#6ai4=8@I2WmC6bY0J{cP>)l0UwfI?oS4*1Ct4`@ST)dx7_SuGAqfkx+CG? zTYWB+O)&V0d6w8aKz;Roq_ zE#ZfG%J~8YTTtIiF>-=R0qsBu8i_m;3jNeM^X{EPazM=MF7Tl8wq7DYS00l1+p<%< zW;2Kug;*0cFE$fgB;U7xN`}8nH50Y(=!S}qK_53MwF!2r>s@iA5Qpe`Ck?F-5Ua!? zjU$!ycO|z}_KZ%BhaF?ePLiJ>#u3yh<)Nw!KuCqA-wa)_S!1sOq|TEbyKqd^>CKGR zACu)e8C)!fQNAzRAQNO5j<*_O7Xwuc7T1iQF084#HEaOf5B6e8U<{w{^ zxtjD*R=z};z8`ipQ-vu|G;I~4DA(e6aa$sAHC?*YM?YSBMEjonj`mTc9IvTG?sFxc+N%ygy zgy!1iE^kt(`A=0-Lg92%w%4n5hH^(`E3wJLxBZYnEm+YX^bYWclwdRx8iF> zBE;%oU5r(_$|)EmUo_RDIA@#l5@x>EPGWkqcj_X6rfq9_-6fJ_8lJ%5MHy#P@N(!_ z0a&2U9L2i2hZ_4vER?FrJpJ)+_ogG>Zukw6giL4;`ELiX$2A5RdAvcbRE$2It9nk zb+3-wgIbiEUKKm0w%J?zc9()YIOMHa)g%#Ds>Ie>wa-sr1P9&H)Jl@oE(cbvT1$*YQ|UJ))2N$1WM^ zwVo!|&$~lQN0h80Ehbt8ZNmOJ=mkWh*fe!nHf}v^Xkt;xGMq}Nr{KBHw;|*c(k>o{ zZ&<#g)>1@pSdJwf361!1f226}=q{KpLi|oIb$a<@-Vct!0m=7;3;f^Jb6-a>(QH!D zZio`n`xUr_xS&lnyt;C}g{;Ob6z<@?nwE(uG_7N9N^{UXd}5IPPn+VOg$ZuU0ZoC? zZxA);jTQK9Uw*+tL1()0r!ZDw!J9&|gkN|!-#^NquHc64tI>xou?$9vbB$GR3f61k zoSs5%Y^?dz)OkgQcmD1OIfEvWzO0y?hx_F5EK!WtWw=TiJg-JtYLH zgvTdck2u(~V_%h|&qv>YxEIc)P!dvG{$pn2q>G%Y2VyvH_^lrC2Yi#o6~exqW+~kG zOg?$Y#_eM9jfeS6eXD>&so>*C-=N{q*XVDx{++lLJHEC*m|a-pwO*l6gbYQmgG2j^ zHoD=2@<;J~rh9*q^YCBL|%ON7IKx{33+MRfG8!r z{EL|IRm7$m=(Hg`sqSE|sCk0H1>PhlIm+0y-A0Sasxl=J8toVZh`?sMr6~GPNQ8vpdhd}`(_v`#lhY~K2!mASA2cOQ-aCNczN6+~ zZDYJ@KD4BtKn^!il%0WgajBbLHb+_3&GFWMm*W#Ivnfa<*U>p z(~A%NrY#YNe+P%ROO0e}U$5(p7Py{s4lXn}bI;g$)gPCq@i`pwjd;$Ud5oM1ZYhLA za+t`lpyIO&2BzaHB2rbeJtmsuZIn}HNC$D+X=&%YH>s%M+yiEyP+Ao%H)GHa`@%1N z=WM^fU%z^7v4scEej0bMhxxc53UQk(fiaRkRYpTj6V3zV)IYy`7jIz}UYN4GJrg?l z+M8Iwu9EW&4KyBRck}?>yb(y;@68-nPh7AL-ao`E3i80~kai%wo_@U*(_6>LYD(sH zs@bc-sE!F8s`EyxWlii8bL<9K7^mIpy29g|ZN2DXui6pR4hp?4j@=U9Q~0s(e;z2P z{2)S6$irDV_R4gY@>b@wT`E%r+Wxj&L$g-eF2$PP=&fjQFsY|lL=UWy9!mb53F_lh zgK4cY0fH9SdzZYbe&^AdZz}L-(NfX(3eLjLB1FTtBd-#%_kFPR=t`V<*&6}Zus3R0 zZp&Y!FQlZR`n8m#+1a@Kfqax2*)PL-d=)rm)NArj=Q~enjE6@&HPh@$5%Od?Q#Irx7!qvWj5)}0&hmcR^ zF#DpY{bJ&%(duwiPc@{#hFH4tKC)AUgh~%=r?ZOncO*-<#D+p51_s!dU~0?@1|a>e zs01zDjMUDUyng!G9OH1I3W?#KjCPVp=2xC z-0wcfcQ~a+ryA1|j&y4frxP2>J-V-(aR z6lPZu4Y?+5dwEx~-}Tonp!tM0$Qn@hX5H`2o{^U8gQ!=CRo8F|mU~(FOiAg9pB|9| zu>D>mJf6*VIK6gv;J;;ayM`e7Li*O_64p6w_3UTh;^i(kpE65$^79KEblumGXh=<3 zDisYZRn2^^PqBG?tznWzvNf;dJ|DvXuAF7+7!KI9T!_5u|RJ>jLWalVmeJ>C1 zT(&-x@r$dElfQ=c;e82FFwAL~IkhSePn;8+87x2+QKKp|je;3`C7P{Aq~T3m;>xSv zqb{zzWrAX*;Bba&$YDT%5x{yEMq2i-==YOs<~+4Z)2VwFx6|(f>NZk@9_B$UU)tl~ z>;g00oC;FAAlvGnvmy(+E;&0+b5_jf`D_818&S$ej9p#3`rHuHf zF>ER%Xm){OJzvj`Y~rGt!$W(2I^X2?2x(0C(xGjFL_%*T=?YtEH%i7sm8b$N-uv9? zjl<(@+9~iW6zKQuYSNY=AzA%hlat#sCvtR%vfx8edD+b>i63+!ai@^1gLAnAYw!8{ zi8Fn;xAKzz@;?fK;_Qb95%664_J!L-!?TuUwB)ST`mYdYrE+T4%hlF&w)YgDr>&x` zu(CZ30e7*E2oou`H~s0x7ksSc13$B4sujg8sfqg0pz*z>IFt)firw{MXxK?RF%Xoy zEfZD8xbn9Tln1+YuAf9==2HLpWK zp=YGp{G>xQb?PCaH0M@LKj3vTtn3C`ab1#Z1F9)T952LjL!QVCmXFL5Ce3!;?|&%j z9y(^p$zfVbJ!KdVBR#uJdeAWVLsW>AiQ4g--$-n}#Zs|QEDiyTue*(rNvPlIhi5^} zKds0AGyw<9OO!Z5w!iQcfqVz%gza&2&n*^>i7a*Ufs{^MNgf{)RI=QC}4#`T3-dFMfT2 zWNXnCs|%{*6bH5BsMtQsXg_!eJ?{&@K9w2YxKsX%#wI4~>c;my@#{eQ<>eL2lFO@u zzXJtmQV++s@{NlwpI4@V^~*Ynb4tW*D@RYEm-&9|LHaR+;XCx_i0d8lb?v1-sSoJ= zm7q6l236mqTvrol&#UFV`3~M*{iS8irCSw}kQ?h|j~-G*#H(nq!zwGZReE?ET1!v+ zQU0_M;v|n|>`DsKbbkHhZsj?0D{LF9xqHw5>>&8Us@>ZP)cY)8ZCh#B z#;k1mZfV$R4gyrLKJTr!>&{lE3M zKj*>C)Ng-}KfdtsPfRjl*-_7dkOB-g+6Q`)7t6L6%(3k z@I1TeA9K0bq%W-p{w2-@TqkEz*UZDeJ#^7}|7w{X2OoJ2;{WduMJ zcxc1^6Ju|r-oe6+Dy|tRlI>$?N%2NSnAz{S5_W4ednz1IQGU;mnA+1(cy+;$u;b#ALQg{UG()txIOURf9V_pzrp3dNzaeiDcDn;PO8H! z&`Ulv`?Zw9*Z&N05*MfHRtbkul=Z9Zu|hxgvp_$xL9wu7)@O6Bx{W=^*fPH(U}JY;YJ-Ne9rUd*1LIn9N=l2W?oizn`56;akSU0glCT|i(p0A|DPARE z5?Qe~W@NTp#e_-B|Lyyf>n#!->V1ghpj*{P_UA~d2NQ8dGOxb=$RPdO{Xt#nULhIs zwMwE!bv9?m`h%sGS`K&3Z~dmf<6(+*T}4mEI!J_Ud(N*3t$F5C*ar4mpYBYOVah|# zcM;1@0gYInTv&*UYc%d3-}nl4x!j+9^ba<|nXGEMZ-&QxGux00 zVQlsdNhbaM_HX0Y17IYxQjPE194F>OH3j>B?OwCYR-&JX-S2jvgjrfy{z_`?m|&q7 zk_m4oD-(bcBtrm2MPxr11$^XP&U@AAkvg<#o^p{|OTQ_pMkI&H&#zT7o!)r44DJjJ zPeKAXZKcd#Dy{!+>1C0Q!F*LK>K}r?Qd_7FCB{9X(k$8=3WMnEwVT`EzD_SRxMw$C~~Ip=>{ppF^O|airzY8tOKk^JVYD=qmqyCF3ky zJDd@&l7?k)2tM8?Bqkrj^4d%HBUfY(=O)ABViu=(N%5k4>`KGemJz1e75d$~k*Rh0 zCL7{Ku^n682f{Am=DkTZ)^n#8<}TI2&TW42oW{tUUN;wYF%_?7;M|H=6mXu)O*1zw zM`hsoM6Og6`{L7b=lw>#Aw?yDLiMBQ@0XEVEFQuWodC!P+cNE~9S!{7_R!0niGo-` z?zmZl+8?%*(B(>5hYz4IiZuzNGaZ&za$J7{GrKXkZvAURXS7L?iqA*B4ayY{vmGq- zxOLo0dJS$5I*P&(2O`ZERR^y?;76A|Zn2&kBH^43VAw|EH2CwM7^>(&&bJoBX5pyM zr8YKEISh6(d+EK>$a&z8sB6XYmL3-qoQy%t|2kI%(;Nn~?2)M`Y1ZijJBW3_`E6}N zG#Ne}vV|FkGZa)xs5<;h-PwYg)U4siqdx(QpIFxr^x~*#YatVYs*ab7MIRs2_xy_u zo^O2FGL0RA2AR${Ia{9#pbrrkmftg>~53Q*&L(-L%Y^RoI>`Qx@Gy2 z5d3R=E>nY-xc)1(fr=u>%TC%}*B*DR_`4i6g>N+QeMQV!=(U@7;;iTq>RdC|Ge;lh;@Y8&g(OOM1P~CjjK#l zB{J)oRK2q^Em8ZN%DULmqD){zG1&*sYrM+lRd+V19U+F1)r6N1lp2E~+(&oWZGV#z zv#`o?D#!UD25DD3Do{Cr*NBj+sWkKS~MSrYk%)(={Dw4Q0b}d%zPCd4( z^I3k_x~SCF?5{JRRN4P4Rf5oERR52|Q|U()vr4>}bqcIl6p{&?0}~?y!YEWCt*wp? z-quJx_J@mqTi0`%qU@8HBo5%lV9J{Qswd^gJ!A1LP=bwwsUIgnDowY&HOQQ+QZaaT zIZMjF%yj4Po!>niGe_0md;?m*i%mLZ3eyIxrq@l^HUkW{&hIr`ELIqdLAY?nAP)kh zFaQ&9Z}dPrA8>k{;$PR412hBo?-8#D(Twr6dqhA%*5pvfbD(lB%k(XPFfm4}4$6H;dIuPvxEb zs}DaC$t}ElSE&Mth5X(~g8bHFh6YH9|H8vB+u#Te2_fdQ+jUe`{ow2zCCACllPUNG zX=#q%*oFY$(>n7B4XDtTS4ldQj1nn~?@c^c^3rl2J~OUL)U?y%03$m+1LtZ*Q_42W z+E6v!w>Xk<-J&)jfJJdPu?>(D7%N4Cc9uZjHqB`Lg?rdTw{8`WxZ8V9==SsNhL{79 zh>MGNJ!hJ@jnF&cCE#{Lg1SeB0r?%}b3Ret8X1ST$|d)G1-@1(WwssN>P!Yev)I7- z)S9*Q3|gK44)j-cyIl3}qX6f`ivLvDO3c)2`_B~-nXF@0UzEol?l5QYfveVYGQC^K z%2XPxX6ncEIV%N#2>T?K5)w?d)|WY2|CDM}*!K>P=jWR`p<)BoG+a>)dOIZt2P2D3 zMe5!6!o#@yh&Mb2lgW18r0<-X?x^$FnclsFUlUaIPItUqB;?|!;M-6;8*a7$U?b{AGUsOcYQS)l^UK5;278)id;=hwS7yjKvuQ|UN%PjjM zA`IGb@-PWf($dzJO$RX~umL=43h}~^G=t97YTDXOl)U04V_8Z;f#|v-d^}9dx)-rg z9PP?BS-Hd)6mDK^QGSn3EH+E+pCx*`mg{Vm*awWbAMJ0w24pmNTvx;OD(F=dpDjj>%ul4j_t7Rcl8?sz{yMMTI^ ziSbVEyDxRTf6SN^emzq@+q}8HHY{mtYg>-SBCbx?1R+mHE9Gf5i}<`Jc2g{w)(6iS zt*t7*AoRm+50$YTyL>$JMi1aqUK0wYm}eDucQ(H#tln7bXS-1O$-VDTRffo;;A#OJ(;i{{YjIFJvSX#7o8W G{r(?skMfrQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..42aed2ed82f9e060a444b32ec6a9b24595e100a2 GIT binary patch literal 125672 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⋙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

KGbx)@f7o*_7)1|rb=+zGp9?#H5%uW z5EjIp`AbNsKTy|K?{$L75Kwd426P`d1@V3ru-9RhApksMf{r~Y_>LXW9fCl}lBM{5lzLYdodPd75Q4(6JDe?#P3~?9gv&cxo#n<@6wkuq?CL zhH@}pY!GM8;(GjG`&V|$clTF{`)2nB*eE*BAU7Nu~QaYn8&zYf2PTJ38JTx3h ztQ%C{_Pknujucn?;rW9N@KNrbLkx*X0P6Jm{lMY@LjqDr$5noq_ci&(*Pjb1fq$1z}{cAaPUU*XVwq+nPj@({(fC z3$(!@I-f4mcPp-ZdB2C{+|+cfOD>vr0+A~oqYrx1^SRLN_Z1W&C`{Xlj}87@6?#1W zL&rk$w@)07Hkr=YI6Z>DgI{d4jlI$yVjkr4q@bJDjX@r_E>;%<1gk-rG|0sXpdTZ5 zWwkCq{_P)>R82I3)sjKhoN8>CgzC=v&|4y|QP-=G3bUs*qFEqY+2K=dKgOHNdN?#h z=Ud3+R0r%iFpH_MvA4SM(pO3O#fg2H5E!`rJE_WSwz6L;g%>qOrtq~*o3&L@aj9^@ z&AQx&nX~Eg?qG>1NLx@FgD809s0CLSCb|fQDI+(+JG0nVd|Fn^%1KF?Mimh<^K=KH zq2!2dr2{YZZrkm+p8FF{AA<9?F`i0>(n@VO;`_B0LmTAOv9&d*Oikk+9Fes5@pauS zSuPj&{ds$YM92{RdfRAjcgCyJpoUjQlQFv57h8~x&In8gvzL*fMMOiGCBYjZkjqHP zp^fR$??x{`9L%hF78UdC&1-pkyu4pbj2(zAGObYse?|DzJ-^qxggA^g?WJ}M22N2u z_8=h3p3ir^G@_ajEObdAvD6rZBmjK+|76?P6wR9r@ABCFs9F znnEr}yfBpT@`W=g@-*^5O+u)zv3J9Ilhh{vFmQbfss1XJDVUo>UYKACc#{V=8=c;H zzdsGcexE+$vyp#Gw$k%OoLwrHFP1B#tikl>nBO$2{ZcJc5GDqMaqhhfM~(@frllE< z#Qt$RCBxI|_F`|0{5H5tg(MRMEgU#Xl;uK4MHOO&b`z2N-hX5b*`4`#E_}R$fxf%D z7Zw@W`y-01?Os7%7l8N@|3_5#NWMB z0YfnfKeS`DxWX|5qZH$gO&wKSfKkK*HDIRLHmh}9Ou4b$WHWB7LSHSzWcui+UrRa) z&ezYUT4fOy{Nwv|I+1OKOH0I`B;g$8LD~K8c4VK^^>Vu*HlN(Nr@QO6!#WW|qtR;P ztAu3Yu&G_20sm*jx(*l<~_tEZ~LkXMxszk zlEo@H-Q+nGuoDG3p`_Dc*UUvuf89jm(`<;uO1HQ449}}cxzyuYaph*%GUti^Eg^1p zriu?@^6(pxGOGgJrp3M*@^vzO{C?rS$CfK|^Tqr5>iw!K)$50zu$RGXF{&v?M1OG3 zj%?^IO^p2`fGAThZ104>(-`y3_?BH{%gfo0Jp6vZF$MKVg5u?1Q z58U{U#9kKuxeqgrACl>V4hCPKNhWV*2o_b0i>5#34b3<5bk9R^Y&RwsD$)qk3K|I|P~De(&Apf{`Swna=sf4DX( z7$F zy<+e3OZ8@+leBeWIQvwVeP3`}7daurJ^z3HA36Mxfo~dwjLzC!%9fBvqGhID3O;2t)|c4SE&RR z02vcSVhON^I4YW7`*eExg=U1L+EzDLr%rUH^5KDm*FZt)Yz6!FHm#5N&K3nW?sDUH zDl6#dz-S9(YBMeXKN+Llp{L}qT&+cEwfDiU+5AC?R{P)JC1fHJzb7EnJ35xVKw??w zBN?k%F;gnXBZ41O62fMAM9EwiN}iOK_7VU!xs|3Hi0d;(qExCKsJ{s0vA1(5J zbTEFK3<1>h3uf{bs8FhHknmsyiYli`44KRy(gg!|bD`6Govz**{-ZI6gi5q+fBF{< zq9Bvi%l79_p>xLzcy!KAgDYK%V1#3?2e`7OMi;x0*?vrRNCQ*m-I3~{xWbPJnALO} zc<#_-_jh0>Q>16JcmtteXe2{iiEJ*`V_(`a&BlH$UYE{R+qvs=N(1-%!$3#D?)19z zIr|B@@Zx3iwo8XL&+7jsOf~u>4*1_VNEb=tpTLp}x|OX{+%sjbiJFhN`Jk&R%YbVU zVID^#7m1L!tA<*R(5LNbsbcAT5({!I&*BG~m$+S^B|nK|bDcMbB}09Gq@u_tMi-cXK(c-$e0ag=_ptXSg@2r7JdUsAez)4_^L= zCZhZFi1DQTe*lQ2M#T^s)@$)%k}JfG%N69vf4nDPIb6eJu8Sub4{K4#dS2^l{rdFo zb><#cZJa9(CfW>W2ha26v10UZ+{YLAUi&--46Ym)WP70YPKpN50?N=s1Me0;oMiHX zpfNS0B6GhaRVQ0#cy``Lx=Cl4E#}p7tA=r1AFkGL1GhY%o_@a!4~i@PB0V^plg+%L$akxZ*v#dTBA8#2f&}N!&_h4iGF2+IroV}*x5p!aZQhFUk9;%F^7-RdbW0Quefo#% zQ22=e9{roUf>|7R1KCyYeKrswF402S79;SdP$C$Pa_z$;6IhIRdK;sl=<93Vs8HH& zb%!B#FcUMoPqF?1?Pir6Uus+I+Z~(N<%Y*7$bL`=VtnE7^u|UecO+u$mLysdTPXLc z!ym)tm8bo2eZUSU8Z@*BX4*zSBs3dma>N}4UE(x;Q73GBF*ZUpX=F0J$#T63J;f8m zY@Z5n1l#gSR+i(y8*>e%dZ)>z=1rO(X)v?+C1Ig+Pjv~{rT|x%5=;w*w!%Ga-KJzj zVO(ikqW^gf>Uw}JMnue|O2yK%qZ;-`rKI|y!BIK0fn3KE$kiPy$q{I?GUY&FVvdjf z;lJ2f2>dOEF))tP*sp(vOi9;kg;W_skzkn{$S57or*3D9r>{G-A5LEVFLmF64QH1v zoTf7IcdGknUfu~9|J}2FfvNfA!)D1TH3LJ16_3@&rwBZGw541qcgsbHJ3*2P- zy+H%n?R)Th&+Hq;=dho!i>l?;+68;H{#-GiO}4p2^f&KyA>3{j;27C*d%eUIZ-8ZuAAO+RyPdnr}zv@NKs57#+McnQTR90V43= z;6Kbr9=jXFlPr2t2HJW@heJh*|D8qU1F*;OF#bvqOrbC_yK=pNAWkH35oso3;X$Q+ z{o*u|(ZY-Xh|LZ=<_dk`cg${;rPp)t63(H_=+(QK(kr9m`HMTDv}o733bDJjdg zuzyDe=_&CAHrQ;5o-KJ3H%nm&<*}crL8;2gt=95`I5=>z2Ll|7BkuZ={*y@v-Kj?C z0g(yk?*dG!M&se*Qv}vdg8+66n^h_l1YiwKG@&|Vuw%^&SZWJpCDPGWqx-}Uq%Yt2AH)GMg8Mfv?xR^(d#OPPwAtbCk5lvDFL(H>yXX9M1gTtl zlrq$~PRLJzca9A^{E+y>l3WkuVqySaHnVlY>MFy3O8)EN+gTQ@L z=)h3MO`+D@{;RyH{oT57&PA0%O@vY~eyXRYh30}kiX-?sZ~DX?^a-hXu_fKj~T<|kqB4!NvDIuQ$wGo8g~wp zFV06HJsit`Df;@7{CIaHJuxx_%Q8geicBL?FeZoz`M%z*gI@L7IllivR#lpuq z?CWEYArB1Sp4}l{!CHo49<2Mmpqb$U@=UK<5>|E!T#u8>%|A7Q;_PdVa$h;0Mnt9u z%Z!2|@!=o5@Wd^r=(D!R{Ua#zJvpKL%jJ(H5U`1=Iz(;5LZZU)2n7X1bUiO1pSfN4 zez64_-(QX<&87<|9vF3jtHKvL)LNc>z+tGxkdO+>)a1}a1>^=m!h_qM*KqG(XlU{1 z0s?PO42&n8J>NM$++Z7)Our+YG`bm6Hbzeqm+|zAYp(k&)5C8pfgr3uO9Inc;IA7# zy5p1tlMdOPM@mnOLbY*p;^;&T2`S|E+!<=j_GrU;0eay9+(*TZ^g{C(b${VSRQg$x z)mV0@X)+Xl+b0VxPKA^bLfZ@1JiqjV2GRZ9q;1t)@q9f4SF+)0a!{Pf8yOE?!;zNy zk|8kTn(3mP2w72ecr}_FKl&}xWjA1sVa?@jH&!~S}q@^?L?W!Q&`u^!hhB_pSDpSKnwo7Hg>Q zSM(QqqxDfZ8HA{ixSh`Du3;t}R&>`Ei@S=E_FeuG_ou?^<2|{5!UU0MvbZ~?*y}0; zjNZV3+BJ3)yTB8N_0hVlD;Aldgq|Ntn&DQ12X}pkWKBoZ_3E{AIGF?8!k>V0i zJJVzxXIzz_Q{|;3ft!l!TkU@)a~Bspo@X5OYdWNZ{yZS)FM3pFbGrB%7dHmTk9mV! zYvKoW>n@m&eN@Zx1~q8Y+aehk+}C#;mJOR<*XlJ>SG>BTwd_=wbljS!qD8`pz}2}} z$@+p<4n23A*kY_{%;fIch@5bo;i&o;-f$-ug0jo)xoFoc%#=u5l7Ir` z(N=CUa_@_SY3J!ceU2=yB@m}yq4w@Po-3)m+>qm5mcgwxw5P)6ov*W{Bbg9}QrdKB zv7W)(%JFQyY*JO;>A}&SE%FA`e9`jp`}oY{cotD|Q%}xJ>=3TlhR7}~8%(AQL3h-# zY>jD)l?OkpN+w9I$W4(#zlTMNR5UR4AwV%{ix=ICMT*S9oqnv|w$FYs#gB0H;! zSpSAo5rNY0?$o)s6A=NQM4Af@^LDfl{w1)|GSfZKtvVAR2_bKo z7ewH(zU5) zNg@aMFj&clV91 zc2kh=o#8EF&$7_0X~o9-_wO&wX$&n=;dCH~K1&5t7Cvyaa8#2Z=LIGG@v1G|6@r`y zs&LNPs<|!s0B@@EqP~SqOv2?bbA6-b{Rw#VuG0JVSDwL_LF=zie4iA|0>7G&LfjZr zw|@h?pAvcLNADPVUin!)iD6#XUP=u6l*qskTZP3OZ!< zt6^-y-b%MreIIE3vkZf(Y6?uY1^Z}8wASht+(@nnWVV#FUdqTS`g}v{-0d1PL7`C5 zsDVn;G(;wK0+ktjeF7m~TlSUIn=Vzgcf_2`>zQ+I&)q4HKH3w9@8WYPp=AqbVM2t8xr& zH`QIDs-Ove-D^50>iN_^pA!q%Mb5j;BWTU@ki#!QC1izcK*~7gsI=`1laxzD9>uf6 z0h%tO_{jAVFW=|*&`YYq-C##Jla9*AhiLsgjU5{vdWjkHAgC6m-5#zGl<_w$61paKT3*@7m)4;rJfQE*UjEP zkX9#P9ZIWZxSB7tkzJH8ry?OP#}W>*2umBmSl)eZnqa_k*#~HTE}An&tc!3wLb_a{ z{ac>cDK_;}7uF6tVnw3fE;W%56de7o9S+kUJ~#kaY3jgnNEf3N^>qJ z)!0eI5@B;KqK?sfx2O3Lnh+@S@wXG25PLTh{UY;85(>R?mW*d?_0^Z?DB#!0m*+t6 zWSasbl zH}$cb@YTGJ0$z*e+z`x;JC9Z%I*f<3o5IeAgIMFNua5vZ46T2YwO4?4YKG9Y%g5*i;jDWxCePkm$i!8aqYxUw z;l34{^SNMDk#xT&w2*w&C8t(TnGsK$f!7+(J7EjB*6W)|q3!p7?RmVUwsu1{mR=ws z(&=UiLK$J(+3?YNMokTyER~_m z0-lPls-tBRn-mWhdc>BQCR_bw0YY-*B7$i(xTG+^q=i1k%#>6a1kq^($|PbwjN1M` z3qAHcEb!J6_scl@q?nbQ`|Sx@ti|oZHnt@v1-vaC8!`Ns7OdF@%OmN)L9%ZtgM=O1 zNtj<1$q#%xAfzx~G1oaY<>F{;A4 zM(8?>;g4NEQn8;Vco(V@d?0USK6>B_^wsSiJuq_!JC598I4gmT`@L+jxa0xus!T8> z$go&l0%L5`W%#7o;W)`_f{`qR^?SU91UWWxR!MTtEpZQ#^!~QT*?CwaC$Si|eKKt5 z?AE_>o7mOm-;L2hp=!jWUi$rFje`JP&&horz265e!!#j?^7|ocZc&3*&!xV8iL2$?(W}sV%P7N}PROY=ShCNMce9tGL@A{&7IX z6L0RV4(tkq8McbuL*&xI3g+Br1A%-w$ndf^zL~zEi2E+hYPb<#@)~X)So^dUm-)Gd7!wrVkHfzK{SLJlu%D*f3Rvz(5N(>?kk8DYXc zdO$_DrR#x`Nzj9o_01i^dRv##GeYP=@5w=vk8zJR(_>T>OHwkn+=@n`zKh~ zY)a$mo^?+j4F|1=pSPI38WQ=oe=kB26vA=0K{>|TgG8GR_H>|35G%c04l8)n_M8fX zr#4X^IZg`_qs;vR%ZnL);eLE$iY}BBhaL}7CYchhT_Q$$Q<*;l(1?B-@YNY(EJ zRzn&ADU6{qP-}1AUPU;~pUMyKB7j8-gA-C8c-}T=*1{p1J3oHagr~}fk+J6Tvy9vk z#VpR3i|?l2DZoRN2|LI_&Q`d;w5H3*#z%u*t{;Z|F(d_NX-IZCE(m@Y;UUpG3`UhP zi=d(sLsC~jBlOC`SHa~&2#VYu&Ts+<`C|YT*fGoE@Y0ZxR_mJF0HcA->9)oD{qswk zG|JD)_#M{fWz`62LK^H4eZtFM-Bc$R3;T5Ty@77jLLp826sjgS2PPctY;GpnERNG6 z#QV66(Q8)L)!9OPkYaE_!v$(?LvoWTQE6d^$jk`b%TMcLb{PtWamNM+Sb@d*mW#vz zowl8-4ns-Orf@Ww_Anu9M+jX66hlJ)LUZ%#iWacpg($e{PZm)!bm7-0(+HU0)WPD?nm^v!7;p?!Njl%ijDWY@mS^vN+l-8kcEZfawBx85 zoX;as_(+P#^qWn0@|W`IowuH5PItjlF#P0z(U_Y>DTy&G3)Myu0i0&%LFv_k@RRKK zNhuvZg!M?rIhae)fbZdN^~F#-A*M9`ZbFy;xc6Bld7>4HIwwu3DAq{}5iMtzj_rJANf zEy|ANM9|3QMRxhvn?$Y3cFJ}zqWo&)Dx~$$f>kq@dcr^x;(1X4k}J@Gd(%x{F57+s zfL0lP8~Mml9V18)W1#W2iWp?IE)(f*AJGi!$w4F2my1Zc_lGO%ZADTVDK8Z+_r~=7 zl|iG_lZBb?ya$}E9^LjGq_KCDdyeCT4G~%@_DUnwvGN3A!r;EME|P!*#M6_^8cI+o z+D62sWQ9u>0e(9Lt0ZMUu&ptU2}AR^ws4X;fPNAl={{9yfZz*9e|uL9RWh+be=3&1 zk-%R@OJZMfHMzeYV^l?F)^R1U6H{`<_ z{XVO}?W6g3D(^1JW)w;H6x<~akI(G0kG8!qVPdbk0N+741}Bp`XbXB>-*?=A>J% zk#j97ia6SQt7hV7y}7CpU|1|a%b=cJC_A&$7!^J)pUl`tPh4YlER~!2;)GlA*D1irnqE|GbFcYq|3bb!6IX zZmTOhOj+})y1tI9qjMuUZsU8OvA*wH&%uZTboicQ#ebNlV3%3w*T5*cJ*agYG`Y+h z-6?-QCIb;0RG?T9eXpLcco;x&G=2cOL*eIk&qgppczj1B6A>Un^!huIbsP%|ogqtW z23~ZtIK!H;#DUaHDm)^@sykpKcup_;x;d<85}4Fc2-5>XH_z4m2xy~x{Y?;FkX#x! zyZ{*s^p&aE0e`>`RNi(kyQHMMhPo(VARbE^7ppe7Zes248CX(%4Ox-M_w`R`4?YqtdG~vy73l|$9u|vlgi4< zCr_JMQBg@mi|>DE_MExVsCLd-XAHk|NT-e+7-N$sUb*h~^-n+hym{N3n3$NDnC#Bz zA%bZE()^^y3xw3rz&<0MTa%jm{)qQQXG!E)PD`(D&$4+2Vp zrcO;f(by%-1dNzaiQBpE4o9;qqW1NIN}2F7B_gakx548ZH?B9%Zl*ixQ=2(Rcc2h6 zx1qK7`w!S&bx>JFH;>=Nxyp#8i4jp1!8mb^af9fuSHRN6yEEDs+`izh)oXr)EMwCmomZCg#_~j`7 z`14wnmpr(1^3<8f4>%30Jp25MM6~9|pC(P2fx`Sd??weFVNp2g)YB=(ORv0!S)2_@ zVC^rz{`k|+XPt4ns;ZcsH~-GkZDm6)xrh>Yk?YNuVBUK3#HK!in3iV10@y;#u&D^ac)g4!&TLLr)Rjs3n-W-& z;#CH$KASI;Yj!l6U86ZmxUy9tkBoK@(&-DCTc6R4$1I-2x548JlVu0-!f3cxbxrT> zRo!d7*-z!EAru1wcjfY>2kf^mC9q-RMojnG6C0 zpGZXb;Sy1|FX|}c>Q)j}iU^US zk&SbkA>UnGKNTsK3lqk1qu?PpFiTAfT1g5%OPGJer4%vFN ztJil>c?F^!+66)m&J~K2Splm|D7@9-zcT*=9`{e5GD%9qzlTBRor3^?FTeWwwb$RI zrqDH=5>QnY(YGidd=}H{+S-)*8a1Y4hYqsfayl{9bd55h>pG2z>w zS7dQ(y1LTH+nl^Iw=f9VSeS36X5UBA-Q|GE6*8IM*rjCdchx`?-*1iyDGX2@##<}O51=lQO<-$datE+2> z$f8=}im&g1n`0)}B=kQ@q|OG+>;DkvbVVo?dJ`22AnZ6X#`r4$sV$h@41!a5WT zVf!dI8I6#Ya z(y z2fcl{>T60*_ikP1-f~l3UM@CUTvUWIq%GnlmMmI`)BFAhA0qGk>I7>a?YkK*E@ z9^Jc{cfE;;iHV7cseoaO;+9$ZG@*J0QR|l&oFY04 zTh*Ezqv-xxN?`SxHU0Y?`RBg$IP+`ab|EesHKw|{`o(AA)>UbzL0-1uqsCx7+|a=n znYaA^1gY<8$32Czc}`4BOm;D{&8WOq^66(^Pnw5Ms}P5-bwmt%9k6XaZ^h=mr%Me_rzYr;H zcQdY?F?H0KE0J#)c_e7(5JD_o@&Mf17Y#bk{HtMNVq#)qV)}hyO~X>&Y)t9pV#_{a ztT`#H#Mb3D(zA3eyH0a$1zhD>T!Z@MipuNkH?qZv#_`eG`KTrJY8 zwyt+Y#U6E@JZRtK9nBo)&^!tw`SJ_1IsLgHtm4CW-q?BV3m9Yf-+TA+rT1H{R`bM~ zn3%K#&rNrP-ua0B7J|>c>Rot0?I@KOj5JEi%u8ls(n1s~5z8cnS%a}w324^J+!0d# zW^past7VmN>t-3|D-_L;!q?6XN8FOKo54sH;z7WAUNo|Y$8$tk*#T8m#o>ru2!&BH z8J1-Jl)(h?h$(+fO#W6px7-?D6VRld3Md zqUOpO*gd%qRhNvRG>A~xzvAiYE3QVJ+8H-%zy3}!L^SGKysT=-xT;~}{f|B!_a2}S zeEu~YtWi-#31Elou9;IcWGswV*P5-xBo$*@dF|xuefKVlx24FZt7@msuDWD&^~kII z53WE1rZ~;E!_b@WqlKff)m?X+zNv%~Xk|mqd@n@TtgX9#uG~rgBTtgSDFG3Qpf6D~ zZaON|jGY#G<2{ODlvgC)tys})0J}4agA^x`rG*~Pct$d5e~u1jOj^DC%_%W#n;30Z zgkM#nWYvYe-s9Uiw*!YIzRbc-TqbsK2qQ<=I{SQmDk~4#zP&?GX7f$IH>`zY-BPz8 zcU>qif0=NCV-v~cNN?va;7zxly)^Md$qB(vL# zpI$1h=MS)_&g*LWeDZbnP(u-Rj^=Z{*ILSoGL^&Vm0zqlI#%IIU}_nePQ{&05#l194B~ z&%Hjn`lryF?@=PUAq*q;`YA;#m*h+t8+h^sshfG7NA1|Y=*b79xrYui+raLOqL9Tw z)+|z;n8}csEcGK6x7MvFEluVJUaktC!lI<=+-k^Zho&s<@l8o;);1K_r>44hRaIB7 z*J&6#=jF}3gfUV`kSTvnO#Tsc-Fy2z);)VuP;-Y522M6h~D^ znxZ5t-S5}H&jha$oVdZ4-jpiA|M>Hm){L17Ulb+58n%`?&=(FT>wbGfVK9f?(xa=j z*B+D!OSjJWVOEO^u|dc2M?p7muaAhrpM8~e{+UX)Tgl6HoqcNfgHH+AtoEZ0%b9eA z&<#oeKl$z}F17X9k1UqBdtui{h`b7t5q7dY5wip ztzkq8DK=7}fZarv9eWE#TneYD(qgZ$LLfq_#w~iH*p+$Bq{mnKdZnXRmgOw!aJQlF zRa@J)qGHe5+Pp|amEyF8ik884L;*AT8_LbrWMcBS!-e5hQibxdCh1Jd-D2CdllfJc#IEMmooN#o~ND5w6!z->xzbm4id`$@Dm~$rQHDS zmvxcPzA9LFOVRRsa;J~Cbno2c1*I5~ufB(?K7YYYFzv^+k*~j(58^{FznOc@RYl7e zq2CfsXfx5Tzc)5-Em$-MZGijybvG|?o_sufmgrm|J{XsnfU4U29aeDnt%VQYt>ow7 zSwF?VwG%#n{v9_K+&zc6-N6@Mk2xc~=(f4eKci^Hz3yQbhCh|vGg-d|kMDD@o18y) zR+>YAaC{M=3kCPwR`CdLx?HuUy~ z+~xIh&w_g5;_v-{Ggj*{`oWoFi zdcC0>n-xaPbO#wR<50a$uIER?dwA-O-db{CWo6rN*dc_TpIgUFDbtsOiWRD%L8vS-PKC<^e9M#}ube(Z>_`Odk2DXXIgypur z`x_ONR1ZP|2w-{deT=Q!WWa#hv4g$ep)n_ASNjo%P(4D^-J^y;&oP%X-63FCUFQsB z?kqSmv(7(*>VXp!fdM#n=CC_YJ}&yz_tZ+f^X{p<_zFZ5IR_kP-FGjFlfE4~&_6t^ zFXD0GI7e%@bnTS%T&3j=HRHXI2(&&9UutxS;KT~>i&qUE6MXI!bd1pfCL+gi{R$S| zT=>9({Drr$>}=1i3rPs`Pv~xr^xDcm2wOf`-K4~yLnK)PnLlC;mj3l}sYBmlFD~YG zn0%iFPl)lQ`hr}7De-JW>m2YS)S+)>Mf*_DA~>T=ik4chi79OzCLO@$gA@~!za8ky z{5xlBzx^R&5Yxdcsvuk%Vt=1|SX%B4Q&IyW@xzZFay*X~9v=x#%>ua--}({q8J=9nn6if(?8dCW?ghP1Q28p zM2xojXW9*bcQEg!>Ds#W5Mgj7LPR!mL1lZ8FjWtpL#HV;mz9be|YbCh~lJghnQv!KEL3;+Y29D z;J$n)0*IBu0?JJ0hMIaW#6ieM4HW=v?Z>x;->97WEw@#uDj-~E^XW4KbJD2UvE zyWJ=)H_En~e?m=2!iZT&$tE?2m>si88uhRA3bVNUO^o*y$tHt+W!Qs5=*8jSKDE_- zDl58qJz0jqK-v(DjBySbS4ppciBx8~e@#sOZq$zL5QCUAc{Cmh>+3gC3i25%#>dzW zI)E~PVQRknA9Y`PVczT+2#aJ{&W2ZG?i_FyW?gbFo(mJ_lnJAH6FdY7jl~u}31RkzzVKSQ3ksUlOe`;l z*VIxy!slm&qoILtqxCd3+zhf`g#K!|K2OH<;O?RbbV zrHqzK_5P+vUZKaeuZ_4I-^Qa`GPrS{A8x4Ap{{4hjk5;_ZhXNV@?Dij^U!=n*N1YK=tGT+p=Gk2o0B5S&0l zL{@#LqtFz#L;FJVq9$>U>xCh47?>s7k5XJj77Iti!>7$+>)RWR3N3_$z&A!#eM=2w zZj;zmJo*=b0O@*S%z)#>W00Id2Dai;ealvAF~T2+{_vyJqZ}>goe!=1>?!XfMA)O@ zHPa4dTm({{c^5w59B@3wt6|pv&`NFXx>h#S%=ZHNx{+`HlRJF^vs&eH8Tsvh=rI&< z$mj_RO(!9+KRB`SD5Br~2ku@h#LoMcJr03xW<=SY5j^cHEfgDBvdNN1WXCM5l{%Qg zT9!jCN&eco(a1!-kpYVd-$-|#=g6{ceX1(jgoAd$l_nK6qZTd6kPt0%PBdvuF{6k~ zO#U(8A5yz^^4<3^#Rxq1EHaGNjGGpG_7&$z$Kscg7;A|A9W+uJGJ{eA7`=`GPc>tw zWAHwHdJ)b@F&xMDCsnN)KEc2AacUMMatb{40)BwK3+|I6;_GJJhJ>aV;LrX3oU6yi z_W_xJao+{@kOjN#c-W<8iE8oH_b=Osk!^rIkk43r$|P?M#Lhqf_e?B9g<}>@eceY$R;NizA`^Jk@=xEB5eua&%3K>DDK6 z$Xs8NxsLgkHxm;R6B82?lmA5wGC~C7I*~{_9!||BOFl6-k@El8yPMudiXe>R|LPgr z6Y|2!c##mCkhm-hdq6^h@BdXmQQ~0lI$r!T-32jmR-^XFGeUc?_59RIGUaycgAb2N z<(`)2ZhqB&N;m)`ulJ)4b>;C3kN=MLzL`Dze*4FpzowJ*YT#~I|7_a#h+idO5fKrQ zvtwZ3q!slC;);}TcB~QbtRP%cjtO7E@W_NicGdOBhyYZiiZ~)xDZbjQzhBIMy}y0; za5vtpORy(k{d>B0tYU2yC14Q|5s`CLQ!j{*_!jrWw>S<1%4>=Vb_GSZVc+%Xc3US2P77A|fJk){JL~^zo$)+>-SEYXeUR zuVC0^$e~A9H~>|8I?ea(?EaVO?GKCDO{@!hg4zF=YUrD2`!I!ZpR$*bP=-^uHBcAY1?h zfO5Br{m-yp2BAx1`EwrV+kPuJQwSCj5fM3+kzd-QFj#^maYlM1)wq*C%L=R_d_#B* z7yBo6JoGnUL#zTQ0loyl!J8f9^ipX&+E`>onmft10oK@-jWjeiWJUFWMMOkIPK|Z= zO4x?<9yWw4iVFx?0uETf$58EdE}+Jm|HRqx>?nyLp?rzIMYp>X=YM;4>Fl+nWFP?t z^K@3EB~>SVf<#0_L{6N6W&OL2#b zHm0pAjN|`%O93fwgM|T(x5Y$1?89P~%)}3V@q-eFpc^k41B8il8@e#roNR0oLC1zO zOot5OV8bDzY-5fvjoX~=#bsiUsl*pbDU|Y7=nHKrwAaJE(6ro@qGXW8^UDe8eeOBW zDZS4v|NMJ61*ZY@0Ov9vSU!o@bkxI9<2vV^Uyz$a4KH9dFgNF+4dCeh4-^W8LZMJ7 z4*(3~pne%qbG5qF3TR=ofbj?G1I7bjMgIvV1sed)2iyoqg9Ty@$q3!N$5Pv1xJndlFhHOw&nN2f*J4F|?<+*ZADpzDPU(!Y269@Ee5xX%QoCws<3LWK9~#{xIT z(OYH~6FsV)t$9SsV+L}K$n}*!pGMUG@W(Zr)e2`gi)FC1eCnER%o;v%8XN~6T~mwl zrPsO=GE}TaMsOTBaR*^f z-$1ZofcV=nBw)Opf~;$+RTRMj^zOpP3Xf&H>J0}10ST=j3T0`qnat|_@5!EA7qDSH zc!IMwt!l1|s;gMOJ=1vePG5eJ{SP_g?GuktEU}qPaKNTF z^zAFQbSW9B)TyGrzco}|9ke+ys-Y^n@uWv^&@9EQsrpn?^O-a(1P%~+-J7$Q_4a}b ztRvcx-8nH$wL%k0Lf3NH)cVWH^21T}mE^r*;7}Q@0g4#04rW{_QY&D9Z3tEkI4d}g zG*L1Zj0|ijm~g<)A?^vrYi8|)c=3b#B?ru$LAdPcek|i|tTzw>g!7`_126+av}!1n zC4ifp>@Pgx_2?r(=}B;g^_3CP6Iz^4T*>u{dFQlm`j^n?3-R|7l)HSuaxh*jgiZm4|D#QL@34t#~P+(DD27!b*iFV0=Zw#!@Y+e=bW8C+voSY zKX!9BCzKYCw{-=kzJY@Q$A}kVHo!E9x4}5!bct^7&M1REDn_Tt*{Fy&2xv0ltiFMn zL>$gl-z@Pi{z&K@04JUE&=duqW5jW`648A;84y2jqIwdrz&YHey*`25%* ztQ5m>iUfluD+3PRuh880Tz_~!7Bos97k8l0)ANJ?&2RhRcqlDqTw_hmB1< zFXFGlmnPZ1nmxY*G2kQ~t}dMZTk$5sNfk81fR0QNW2P4DkVJYd;eq z*p(%Aysfs5yXXaAL5mg=KM>xI z*_Xe%v)NcxKH2@))BBVJO+9<5+m)N#cx6?^nX#7knf?KDLye{B8h~}OyB7wy7%s1h zDJ-ycHtUNH0*lew?|X5N`-U7Cc6hWk8$dJj;Uhxk$T~2T9KmeB2J?eB-->OUNTQL8 z*V!1IwY9<0Tnn;v-@HeEL6SKS%2`#**({i6z#ou(!FuV^)3T5zClic_6Exq^LgtTZ zU0PV(>@4yVI9oCa;!-T$&qEXla2C__oA>@vH?HA1%np3=*&X4)%IWs(AOar*yMw*o z>GFLYwilPd4|LydL1qPoOP8O=s8nc{LZnPKsWUeaA6{uTsJ$8O2rash%pHzmKZgWf_Nrq z%PtyQBcu94Aj>SVQj|hS^ECoY;Q1NB1f(uXg<65Zci69$i`X^R?f7_g(v) zd+zzZ_emr6(qH4JPK$WA`+XNZr6s8zu}8$aY*QfBK4gFwNP2?bU$lZjR3jgMm^6;p zX_B6jJHAE1*jrgqSp5}6k8(%;^%Fw3&Z8Qwv?zW_!PFa+6T?5@+Qiv<02I$I9XZkU z<~!sHeHvI>_Ps<$rd_1NQkf%P;}?=T!!YfDSlV{@h;n8R9`1c^OT1Lsiy1$><^GW%A-*Ug5&M;y?hn z7f6UiH^nFP1Cgdz!2uF1rxQc@l1taD;2voV;Hq@#>hF@h;iKTiJ9NujXD}QjNFbbC zlY+Etuu{H@V8b*$MM3*HT3s`F2wwM6x+bW{E2Smp&(C`&-xm_An$o>^GO~E~kFD&nhqd{aEEswx-Q0 zX}47xFl0wr9UZJ-`h9#XS$R7PBp8HRc++(B55W%S?^o?5v|w&*|G{Fu95gsY?U2Xd z&9gdAyvE5PO+oEPPmF%%=iF)Q?_WMwyR~ZfW2Ex>U-YvH!&zJ`5J-FJ$|O@j3lN7@ zED5d`hFw=NU=v^o0Np^+ey=z0Z{}CK)7H!5!8z~62NVHezz+~{d3cSI0jKosjx{uf z627XD6~#Bcsh=81hoSaV-XdqWLwk1Bb{*$*M^%4bvR`^iJwwE`N&@ z4q;h#@J*UZn*F8yaSz?b;cv{RAlk5H$G8{vkg3A~vPK@PlQ%;mucWx+LQ}86`9UMa zt==8E!c$i2|J>M-JAaJ+z?lHGWcqr2<3UJ1#Dz99C?)cY#C~~KTpwVu@ zw@5*h{>K)wdvWF9-pTAhHh?Pts&B2>MfdF)r+)aO<@rA`TD0=9ZHh!XL9)n+3Jnhd zISv`hMmKGmfu275MBTYS4%#ZeiqkC)22MOwP6_kl=;TDwkkc*7(vhVL)j<`1`{Z{A%Qqlzx0c2{kTy8O8UR?>!yt4I`K$+ye2_OobmE{s3 z07G+M4UM4(uVKZvFQTb5Z55szl(yQ2`?u$RX#$t6(F3Zjs=%_41tgdBv(oEHe=0>v zE|oH_iRFcg@ZYY>O zh3(^$5gfd^Q3ax zbe=lR(a7}y{pYD+6S;IM+%NfbsUYLwa2}!^B9-02OBil>e}^KWuhZsR01y{}I^aE^ zNr}E%_tnt2iolEnzHwt3cm4_z&3pGT!ze@bTlf7jG{2_ep?hH%_I3Kb1^V15avS!mewqreT5 z4eoIuAm^hAff)vvuAzM6I!a=>FZEut{eiZFuQJw$acn$Z=378+pf#2eT@=R7ELp{m zGyI5$34mECI!?Zx&XudDR&3kAaJsYKThGc(yPhh&YkBzV^B_TKt>gGfW{}{w0Vuue zj>yv6T3^^tKPb~#kmV@%aK)BdJ~^{&HT%sBhZw@IqVhXm|F800#81CT1yONL#szXN zab+dpiU(_%DC5lHJI`(SVQBsw_w89u{qO``D75@?*1@yGRL!c8FFMaoWXVF`m{E*W ztGlz7=~g0(7eb2kpx zk?g9;64SA9)G-kX=U3$$)&wR^R09%1)U904EGW@E&s98l4<smNjP`AuqAcU z28bvSmoG&o;bswF0SwM3Bikcxg0pbnGVq!rqxAvFeG4TC7E;G+7e)C z=s@TSKBPuIgqLt`*FdhGb*Nt5(%>XQV6@p=2SvA=uL_QGa@I^Vhb9%g=J zxp`9g(_qQs z8WZjtvgdp#0wUm};q9>qCtz5L_X+@QHL;<)>zgV*pbQux>90o;_79}udmiX?iq+co zprN5LY(Y#|=Jv(?MUVR1 ziO{dL`m3R#)nAPv0Pl0re*E8sU!BT(ZRp<6(9r6yyZZaLjZ}Y8=tqGUfjs!~r21>X z1WcR43g=@^3{$xC%rH<6!w00mVn7HODfr^EFDUvu81uuxH+p)Kngtwojb@+~=+5a- zElbm14Gm3yH8eCdG(IKtcXOnw7(vos9|B(Zy^8+Y<_A}SRX~VdS}c&xl7*utA)M9x zKq-s>V8VdthClitd-v1YL=eVt{QGP+$!43_YW<5RUx{zV7vRC8AcEjUJbCpjco4jL z(TiR@c@fc*2lZlXw`rO-Y4#BoLRYq(bZICi{5}k27-q6`*~7!|+gZEOz5OW;r5AkB z2l__PhTbgZo6o*oyj^sB?vLmNI1v#MktJhxT#JgoU`DvQv(x7b94~i_JO>AJhY3l) zajOe)rM@h&g&yeJf*zzr2RMKX`lZu+30@sA6~Q7RA|flK9DWE3m#njnal~7YKm&{^ z%{?{-(M=$(S8;|1t0U!@Zx32va5yV`cVP-X@1Jo!8X_VhB1_9^&T%U0m52@=;T>o~ z!yU#XJ9ag*jmlTw=4KcO&a6&rKnh*x%nIKj^dT$yQ19xlO<$4oHrO5NCWy$2>2=q1 z6V97eTX1rjho)6$I=JCY@&sZqavCGnGrN9Z9Uwl{_N`NUlh`1IHAtWd4twwiGT4Xw z*rHDUEYK)q(#SI4BPM< z_8|`}SNK6hL_}mYIYkT9tcwar7&P$?7#I!*q&v1bwB8guF8lz2k@DY8J$04j!Bt<}sc?>uAZItq-Ej zk*(!e1AzFf2b{%l0TPHyG2cId_uHz!U-tz#B_BjYL`0TdDE{gvObN$--S9S^;0+81 z+}YwvpUn|*4o-FV*UD|)W^fDE;Tl|kCQRTre1{(}0uOB%mz?%;MN{%Y@e0^~`+ocWV?7cg z%cWXj{$=OPqokw}_f(zk)4aCp%m9fX^*Q&Cn%gzi zw{HEa>s!D3y>}@5GMS1$F+V)urqJcr zHr_gh8Ve>elWBfvcIe9MLj4BEAD>@*E}0KMVI?&dipaeGanq=Ap(_Wm7~w_~5xM@> z?Z3V1{}rPtiam+YrTw@6{%RJhA2R>^6uXJQr5Cym?2Hn$!w&VIe~xpr6r+E)z3I-0 zq00vH_DQ|;>E)#Sq08G8&!z6|8nZa^tgCjR;&FS@=h z)XHEcnNL79qYRTWW=u`oCfoik(00l?^TZ?SI zWoVt=ckI6o4tqK>n@ulYq104ULhTiT13=B#QT0ze0Ezhv_)*=%6Y3hq zW|xa2^T&42J{o-Fe$UU3i%oh+rqb9QxQ{zZ1pFvLsjb$!9p>CG#l#+%;ri); z!N;dn-83jZXPzZ4sZFD!CuzrY3(lBS|I~C(kK<89xme{(T4KRJsz(m3fAV49xo0u3 zWio{#WW0shcM5Bvxmq90Myxq@-$Q{*epCO%gT7y$9vweLrZWI2uNy~+(i*w>iU&Z$c*rWJY7=WD!4UWaVLd4 zski|sc84x?JJbNRKmbsHG_Va=3q*j-PRQ9dxFhm7<_8Cd9~cg5yAD3kd-iF`MT=1a z6FiN>Zco1ahV}4cqLCxo;r=%!7A!=b#PctQ2M%Yhi#aU_6i6?9muaW)RYRlW8;nrI zQU*-iCjb6QtBDb*boc zvDz*ORq>vF5;XxI@BmWFRs?=`zEW91o$9~foYWgjL4;5{cdQwAn^F-#5fsD~1Nyp; zIsz^igaAu1iUQRvMccWr*D~W(oCAyA(*Tp)H|v^i8z26|5S~h+~;8xx1T zlzEFnKI=JpQmF6sJjd*sFF|bUd5xH83|~DgGUyg6D*PzWw{FwdZ3tX)fzUM9AqRVU zonVbHB%XW8`-@Y2XP+i~UJzA55#?evyXp(iaYw10IsowWJYM_BffU3j#;tiyI1CGX$x;v|3&`; zl8(rw8Gn&-h zI$ToO!Tdl(c@wY(Q~|;Kg1%`W0&Ftf4jDURhrQXi?S^2-{NUj51Cx4nF`tWkcxP9C zX+#=P1cg_0%>AC;J$%191Aw|;yUOdX3O?FUJ!)9X(|W0PP(hEVO)0g=kYmDRo^zO_JJ|(W4g8c z=`VaddrtZ}1wkRRrFPuk^^=3}ePmXBuI^XQtRxd!JgnNKBTUo-I_EZSfnfmfo_@0M zxM4`-p%6E=hp1g5H~cYj-3T_4+7Ua8Ot6b4`~LgP;M}QHLeYDt@r41oTeTUljQwK?{U+n-nM)s}#PO&pu~xa+@zv*{{Ck@lgc7$a_X_5ZU@*W+S)o z4G<*XNPYcA0E{Si_rr@1Mbr-MDWf3-2xEB4BNhGoh=31ya+|lh@-63mrv&`^#?8)8 z=st^-kR^)FR^YZ_Pa;`V7?qaxwTj3ofrxf+T&c|o%}}7bq^{{wGxG9RmRF)G5CEzH z>xeoKGga?4AYQr(uVa32aQHF92R1Wyo)eB{I)@54_1Y2?FpTIuQ{3GT^Iv!_Ow`UD zC~mA2EXP>&ZDCWKz3(}#7X>ba@Sb*(y8nJKNxiYmb80U>dRg!L_1RRz1*ag@HUQ?v zn3v+vun71qYpM0TXWA^a=+*wB@m9n8C6nAOOl-m?%CU8!t0i zd>?u!EOmntr1sw1YbV~pBx~YtFJWg%zyD9AzK&@*%Qm1?S7tx^94?o-f4k(u*C>5B zMojC4O-Gsk*qx1Mz5AFW^=;c**$Iueji&>cIqB4^Z&qB}52}LQjdu8Pl=ZKbPeAP*g|%N`@~~2D?Y;B52LOSa#lRku3!Zi=MGli zI;8Hw2}=8Rv4&a5V-u^q#GVt6X}aTHjt?-`PMr!xK?!XQW|_+D*hrs|N}&u|*Z;C^ zDAMxu-`q!bH6?QK(bB^0DIR&?DLcQS`z$gC$X&KQi1ME*Fam%FPyqp{Kv!G|wYJIC zC8hWbC{hXuK{T}>f7KEbgebE4RslYMBu(MF1&9GTo96Di`N2`Z4i0-FsYQ!rOACDn zv@~#JGx_=w7zRvIue_OCw^0&`E3Qd4uI)KdK;*an2OGQ@wMlao+!bHG&PN$=sW)$ z_p#jpSQbXh)JTkE5`tP%Lm4OnsM3VjBLV>m&Fm+u&{iZ0r%Bb$bF_+Vj$109=})$* zarkJ?5TgFG>!_w^eD+gKx8KW-r8@$KY-K0ZjvG}!Z&v-hnHBvmjov*u_tm%9-KZTq zpaeEp`2Fm|{E5`q*X!YkwV;I6MVa?MGFlSY`w?C*L#}vIrtrF961eOlX1k#({_}p7 z{o<=aM%FG?g$P{nySj%b24_uX&Q;$YQi63Tp{>Cze4ZT}nY0>U4;{%&MF%dsu;i3I zGLtSI`ry%=U(tP+G-MivZlf4?ZJl5jWF@2qsaT(itnh|i2SnA{EL?^ZIz$d&fdEke zNFSsCh$yl7`prL+z;<9Q5C*bnOUqEGqU!+(JXb-#tTiO z!51dX((xmX#apW@@G0IrdcfPYvubV!}Dx8$V z#AhqtcWFds!_v#%V_K9JN$iNeJ=}EHM6N=>sXTc};bvM<=OD6)zJ3#jamyvo)b{(y zRH~4*V&V;MH;6r|3d2AiBN~MXH41ash0~x#;qiC*$ zlr?wW%#P)pC;Lt)g_n%x3boVzna|c>w^brH=o zRYUt@?}s)38V{@tJ)x<91_mp@3KyEf7m6ZO7f7pPHMeeqzG3g zgbKp9v_U&7aN$gGb8y&;CcV8!GXoQi6fRbwQ?@VxP6^ghuvYZFnA6)0CN-lmCbyJe z4FINfjc7AWJUx!%pR@o%%JjP*=C}dN;m|wv+FRLwea(8A&1Jv*2Cc^1yGQQJZ&J(N zrP;A`zESkf3H}StTi7Z)mKJNZ;jJim}fKpE&+q`|Ry`eRi8%p%jG zK<@{LEi@TRDXk2v(G*L+{Q*oo#~-DCw~ans=sKk2=ri>nn?(C2q!iRT2X^5X*>n^; zmKIXDTm}!jc)5Kil)}E{94UR>M%Pad0uX=XPaJu-tj5-=y3Fp!rp)3y19lTsZycI$ znZE$K&JO2tUH0`#eb9a6Vez@oF!g9nf21b_dp|rc*P(|b<~>IR%)m%3f6v>yr{(R5 zKlTh2Ftu;u&o8)-?FLoh#pP9CaN5KwBga~kfm1^H>d5qe;ezgLFf%@n);DZsyg}C) zrTj(?n4k;{+V~?+0;n2#P2IG6theIIfqfYK+;Da5;61GuZ)$32eqGDF=d>e!#s)Y) zr29Hq$RvyuqybqFe!Fg+)gbfQ*DNBfR5qzy5~4N>B?*xNGCV4^*1+ zR|6yvHQf%|ffm3hZo$r(blW=P&BD>d4i0=S`3Kh*$W_eHT2@??rRNrxPdg+SLRX1?O1I|4{ zHYW{((vwSG@GH_w-l-he&!(dSSN@(Bl<@!F5Suv7ckY??ols2U$gx}kHr!`W{K-F8 zTz$E_+u@LsIbh0h0J%+@tz{^TM2eHKWHg4|g%DLYUNn7Un59juZQHhO+qP}nwr$(i zw5B<2+qP}n-M7DU&)L7l{7@9gnH8`vxH!451S`^Is#pO(o%-`)GaP$G_$s~2OX+uLTu>CAwnj~N#~mJx zgV*`ftGl3}WBRkXaAxn#Rd#KBcb7r4sKH6}-CDdx5X5U{s)FtR2uX|EZ>1(}DWZ;! zX5oG^e_BpNN95r2Swnh|RD)|B{X}zBaRpo!KodhI1+i>jzSFL&dv68a+YGM=ocoum z7sVIppin@SQiYO*KvC>YlxK?SDYbZu!N5Ym9e|-)V^L;72HhuLr7k5dtRgmZKO}k+ z4Aznama556?#9Mb$zRUT1~Y;OYU(bp+~dx-yBy>lvAuavvL^lJgd_d2<;YXCU#pE~ z@hQr3u}2JViW-P^U4*hV+AnQW{=AHJjq{4r=;3QERA8any>8uF-ImYt%zi4cmQ$fL z#Q^;i@7KDVgb-325RoFFJ8!8Jw{di}590?Vfn~L&z!aX6N@PaC9e)akcMJ4*Xxt%5 z0Lco(NC2ZrL7EAsnYux{QMPNjA0aGi9rnW~|83okqY#i5ISG!xB8z$A((nHd877!c z$&8{+D-<~d4i3(i415jfEbvi~>Stto>v1?y)ez^0zoq@E`Jg!&Q8EltQsXW&02aCg z5o-$0l1+kUtH*KeYXCW*ZW=kg17rV(dMBj$J--2IKHn_J>=c# z-9Rg<@ay^pMts}w@i16**^q&ykDRAm=0U@ErTQMEDY(7S+)|rceANf}*)+_O!a5U- z4Ir@qxbLSg;M3EL(G;pz6Xb4ckJeLi95G-x0!GQ!+0ZnABjWa`Xsg7=-6DI4ecYLg zmxIUX#O3AXt(Azhj-v5srWKITev+x>ETsJbZ5T-=E0@#@oz<|K*h#rSEE5w%;5~x> z1M<0lB?TaoR2qQ9Zy&%k0bvhz+_^-I2-AN&(=~9LcNiQY2tp8hV!R%=SA^emct*HD zkdPp`U`%ad+9ilmRl@%W?UmgOkU^MkgU%KnUo)?#d97w)LYSBuQa6J5zP-vG8m+cf zngK#i#~@q>Kmr8P5(qSbTzQq)*@4<2&u7HH)C=-~2M8Tj-2Wj|0}kE)<{Lr~hGaLj zw7k6D6@Puvx1Jk$I7Xx{`bQ1n&^!^oa_047P?2DG6yopUc7meP0{$HLHM+c{NCL-J zRC88Jg1V-*q4+C1u{u$ao`Cc>6dD79(d!4(c(K8h6b2KX!JSM17)gw&-#gwjkONAT zU%`g0ZFV^Fhy5qm=LwJZb9(#waV(i$US7U~6&`)YH{ERKDgD9h6NudcS&V{~2I};q zA+p5)2FRw$%qgLp`^1e16IDr&wUV$nCd#x5eujW?Z5n%{jerK-2GBP6?=>UpWy2yR(%W`F`6l5zmz(&gn5;lg>7>a&dwn;$7Xc+YBwj7$GvEhE4&)%2T zvK2I+g+nOI(3xD=puMsG4^wiBR4rSLGxlJRF@g~Y%16CDfKUTUvto(2`47Hx9Am9i z1Qcj`%V|kS3?h1fdVL`$p@16M04*F%>nt&AGBX4jb#4E&2LHzagl#WSY$yzDbQ%~r zY9V4+zc}&|*z&sD3@7m6gkCxo`c>cY0p_x%sy0?}R;z@@QJJfu|C_L&N63eMenrGo@|Km@%=wE>6HGrN_)?$O3`v_+bWuw zU@wfdUu(9`y%kvvql`*%?IlTtNXHfy2QVg3)G}%Y! zw|01b%kO@j8~NDzxqWQpcRI-W1OCDP896ia#-H=~-RS@Mc=GT2dyc%3|0k8*{4L+{ zIeez)bB$hu;bUUw+5dKL{H^Fu&*$T9OAj`k(B=$br}5*w+mIfDn)Xh=`yK5#n`zX@ z)@HBsHO~ER#FleLaa(HQ2Wxkzio$~5lmBW5+9^dP^XJam)b(*LO{Axt-Yn2xEx3CM zEX#T|&TrBI;OcDlqNaz*ZbOyL(dDv( zdmU9vO#=wg2`=lFab@j0Md|+KegMRXFJAGLK*B+3a^;^ru?R3s`wOAt3%8(;PcdwA zQdum!xXz9m)>hZ(bU_aMaoKKIcK_KwwjDBE%GRUHYeUejzX}@JlyUTF%Haw&)Zj4& zBm-{lPr?JYQSO6uF^Jz=zT4ZGP`B%igAmu{bq#+TZ)zaz5g?vO|6A{;S-u(8Jp+?5 z;{gLABe-P8;Bq&2L=8dut%m>F`aY%?Qm%!)ZPUDkhWia|Md7bHDPi2M?T8)%!kD?xh|Y^+O+l23^xoI71)Lor&Il zp4nclbI}vs%YKaCqOO{K|C|AL&7j}4?Q_pEW}tI#GJ(J2yttTz4BI9SoLpu#AY|## zN4q@zE;ev#pG?5;H_Z!3XRpk-Cw{dZP$@McLonZ=onY^3rP!^f96wQ^{yj8qB0m9o zSk(Dohm%ND!6_;GB|C{Cakcj&ucOpy^nOUD>MoZj5)6(Hc?7EE3tuuNyO0sgJHAaB zm(^*}W@^!wn(%rmr|yaR0{&2Pe_l~lxy`KcrInGf)^w;R^(LDm(L@1DA2*%Pj?0be zGxUTJ0~kMFt;sRlf-M=w$Xfd-44tq#Gd(@W{%3cf{mXMW<#6?2&5_)xX&{C~*xXtA zDJ^%{b4&yE_EcE4XL?tyN(lR%k9qLWd?=rbM)Wc8Dk#3?MYs4DjFP+Y>n=Pgbb3L! zUS(JWUacN3B;tq|5w3l{!LNwU64<)5rnx#XC~I>64w{p`In`k#L8O_jC#K`X59&Lo zw+(rHH9+9n$fZBul?sd*2%TNRO6jNJBaE@bw_A#ObBNE`sB%jlj2_oAvH|=m#ug<8bR?V<=Wtj;nqOuIv01HR`Egt?ShvBHin{ zy8d-KU7-#jcak0_yD@d|Hu?e;(V`+^jAO^IlH)yH!FNTbt) z|9y?E_p=o(9e!)foR;Qy3fVc7{OjKhfX7VtQvnacb8j3&Pb6edh?z2+{?>?{;o*^4 zr10GJ^YM;&hcHM2ym0YJw&Ojrj=Ff3@Br&6F09TI8i`33IEc|dfUyi4FB+;5tp?f6 zhnvo?Y9)NJ_A-I?mt;D18b)DTW(8GYvRpyoN63l7xc}8#t&A=irB3G~Xs}WnsiP~} z;MlK_6pFx(=$>?*BeYiT-j}mq@Er|rNsy4pGWT4y&ap2HwT6pBh$=ut0){;C<@FFe zPC+3DxGGZfN0mvOVIQoCy;8x^JKzyO+?2IupCR1gM+F_5_O9I{`M0#TNTLaZoaC^q zytbp|KvgX`KPNI-g3D5iP6uKa{XohW6c5#ADJZ38N!g`ty*)=W-tnINp8WmUl(cXk%4)vMQBg0cYy=V#9u>5FX21M69bOMTgDF*tEc zX_NkEx&3$PR)QvD_)A};JSvpL9w5=C@K;v!&Bu>{)BC_ zT`W!2pie4*I`N!y%K9!U>NW4Pol*4&b8yb!wJkaw*tCZTue0ATra{kF{$&9D52=BJ z7Da|~SJbT+ z8;0~05$lTo6%rbGi(h0u6s^Gw56GXlx`6(@|Jd^_2L#n<{%gKnylv>dXtP1x+=6^k z0wWuNI5FMq=2XP@QO8}3$iGpf9r6>*AFxb546}n-fzK+0j!t|uv0s1_EBhakK4h^g z+lWy>izQ?Xy?T133Ii+91G>e)W%5JZ{a48ydCRp?!Fyg-O{b8p$~)ZfkzifXA2)SK zcd+l<+b{QOwS20QU?=1g_PaN!+Uh% zmg9}L>#mTDBn;=!9`rDJ;<~~nW?qYVe_ubdq2#c<$#=G&ATC2MG}DsF(Hf3Jo9`$$h+C7ojLQq zV9JD+lpqq$)&FYC;q(3N>zlp7`}6(Iy>n&pfS-4R1@80I^h30ccypmp-_S*L(rYWg zkHB@4off4sg?2qrT-?h_szzE`y0A5-0HcdYjTo7f4TWkd?w{*saJNpo9m+0VA*${xdG8)x)1ElEWk#?DJv}}!?>4@3eRrxWw+~#G8zC7g zYKhCj*1HEMXTlUG4`!zZDcDP94BwG)xI+<(iI+%EYgCeCkEr<-`PBqUI_=J!@AIyi zl{2_0zO+=@gEqhDV#IRvmt@Wk6@)gQqwMw*x+22n`9qpglJ2JGs_dJ)o01PEuPt^uqS;M1y)zG&cwu;6S#YMWVi_yWXPzK@lIwZ9G(JT7mp?e&EZoy-L((U6iQ;(jo) z%XKI19Z2!n-;`=dAg;>7R2l2bZ}Ry!Khv^AZ^(4o07;xs$D-=`o4!}2Q~2Ce7s^Fu z@k7w`TpcfI7g6-xEM6yH5{PsbvOhkZvdsiAgG$m?#h)1+XYbjGfrHtkV%F(US1>6; zU~41f*rnUYcy3$KNXF!I?}I9eewR}pM;p=e?Kj#h9hvmswc6Y>+{n)Ol6VRB3OZEe zHz`=zZPbW67849KDpVB$#1IB+!!PM2DCzpz`aL^*9@Hl4IusEVvge$qSy(0In5WcT zX8F{f?cLXxoF@KEB?+G&@2MZ7H%Q+P4_m*jjA+pfKNRh=KyrGYcI6zxuVRKOxnfi} zWPp_|*heu7KvDpkgE0^g2kMB#Kp3dQgzKk*>WB-?>$gpY0Ghc=3AqCeM@Ad~7C;*5 zPZ1383t%BK`%HWoA>z$n`iAy*e1_T-uPCZRRhg)?XG$LX|HFWU-1N>=@Si3a{ZG^Jw#v1^w7f`ftcLUmj{i?&vhS& zCnS_513O*W#Kh#1-{A44Pvn3f?O)G{MxN89W1m+qnVfOny424&-_yGLDACq>NkHaG zBp?C@;&AEc)NmQnrE}cf`aM?us31bRVVw| zeoPt`PF9zFBZq%bf8NeAGuNxC&6q=SfL#OI7t&iIYVXR{$_6}^DS_T8J+#U6E4}}n@f$J!kU(^d)(7RRby+d(?2eA(4ebfVA09%5b4eAJWsV4Dv zHZSDK1p%7Ihq5H590swJNRop zk$z~pucIHyS4>b~_`i-gnMU7l0;9|HIDM}aht<;;FlhjOL4|E<=3b@Sa6+3OnzrfLr?EJX;wl>7;4?AI=+wR zh)V^h>giFNmmT)ezkc*M{z)d`kwAr@Z0L9{St!>y=lF20u~~bp{gJ;UYuM4hko!GB z%jeLLGGWE>{CgjY(Oe!_^PH@!b#>k<|wsXht9ru6YGe zDpiiZ_f5%yj)$}Oail)fumcpIx`Wiz^2D`%)Kqm-$GC*2{}=km(-XnMsxQH zH~EVKY$b@Rw@Uz>f&Xqqhi+LW=3hJ5rYy=uePPdq7sEzDAyEIND%AtRAku=v z0?2A-3$x+GWs25j0f$faVt|S&AzCL^*q{Q6&Le`F7F?51fvuG1C@b z-u0Sga)@>fhAMK|uNAaQ+8AkNS9*dmr2=LV#HLV^q6W1b)I=d^h&Lx$;R5?JX$ukF zplb;+i-~;X!fo8^_EeX9<)eUJ*XrfU4UJskROn~qJTp4mFm^Ys)0k_(#j>R+};37Ctrg3mx*TMsuh=Z&E= z_=j^3Hub{T#)loNwlx^5m)1}T7vfK(dz=wLTw&&QIe3ByN&M7hzXriNjq zz`LJ@V}lsc>a@>$&98k z<;A)Kmf=^xJdj33mr2~^NHT&&axFR)kelM+Dbal%q;lOo|Xuq{*OV|F7A_1$JWuLX47rUb_^Pxl5+1CS_a!42rM<2&&`0 z(;kRg9f^EO^FJ?g-A-EXRU5YaEP)9D@BsDJCtHAw7&afGh{%98 zp{>)v`*|#b28kc^-Q1HhW(XX`pKH%_zS8_PXp6QBJ{y5pVNT3qD%H6-*I1f8C$AFn zDtYD{B=frf5SvhZH53V9hNk62_jAAN{NPt=9;a0hrVjXd&>2{eOIl5Ers^Oo_Eawg zErnxT!MFe-1!YkI4kiW&2m%NyxQ}gJ`jsr&l+2H;lYgUnon!mAu-hHD3B8N-PY>88 zq!tVf$T?vvr)X*WSmN~OY*IaK*U%Z6v5)}gA80s?z<;o+)EL0?vnD=A;QuE+D64b| zuyrKN?*tI%g?Tm3P%UhL=jphjcJyI<77;~|(gIc_O4ZFxE&aL06O49U@xZ^1~STswip`?_Rgkyh83$Ku3~QqMwag2%DR3M8|mCm zCA^tZ_jT?6OdM}U$EBCZ3iK&JyHKf&(KN_4FL(+zj8SPZ~w~DYFh@hu##MYITf>Zcl81hjMxES zt7h3{tj_1|)VUDt=+do-k(n%0g2LFI;07em+Cx}uxM_=mFl~Q({m)vun=Borr>+BY z4e~C`Dgc@ksV_9x{P&ld7XP3jVxEfKYmC;%g@Pfno(CHy(fP|aqG0RoQHiC9wP ztkix~;^gS>N251~pK6l;xCP`aoP;(rbBI%+ot259pTpT}TDMZ+oPvQla)d?Lmut<+ z7AdTS09LA@TS)`e+r!v@)5nW9r+eMua1;c5iU2FnDzx6zwama=SZp9Y$3Fl~lzl_R zOtfYAX9yZ#3opp1kl4^X$Hr-7jl%o~soRwa*xq_G{lKbw6W#^zA)(|=2}SN?Lu}=S zk#AEw!zQ*!>%;|6lx5*NsLAmeU2s1bQ1+Rw@|hy+`A_oy!A=C;2+;{)h+ELRjPQrj z`HO=Vr`S#@(9JCsWr%u%C@dNTS%<2+3z{gwrs<)F2(&aI3L)A+Qrw>zn!{nU!i;qs z#~x)>&F*068VgM!N;k{ivBA*B;_mdl3o4n`$Ol%n5jUbL6-OuU_X0G9T<$&~7l6Br zNE8OG;FI$3e^&gM=0D|ZUz{}b7j$;W?T0K`Ek1P#C5(;5T#6_#evV>Kz=-vv6g4ilf z={N8aSv2}Wpr8h$#^TT~{fs`aC;#=p*#Wr%;R+zW!za+rb3khWUxT(i*}4W8p_{Xn zw%^72z7+@+dML9jhi3-zKTG`6X(V2^|y*!p}yPgN^jZ+QQorD_v~ zmBChl4_bYKk_@($aVI&)qol`Se~7A{s3wBI3aI)t5H3Yf#R4El+~B~@0Hjt`1t3BQ zmc8u3Y&`xRE^9_!#20rC5BT>$0>Ko9_PYns6->Xmn}tO>g?(P4Ox|5P*8y!lZJp|kPya3Pl7EQU@tOfm|LrR{a{A6448GCh&ifpIC6a+W*Q9*?g@c2k?Dh0XG(rsSD=!+| zHpzecZ#W%E%+nmey|V0EA-R3OWzB24p4WllICh!y-(buR_QLV~Zu`LFxvyK{{a**Z zcfEOT*jQvbXkTYBYF(ux3t_^Ik3_Y{zmi#IbeD=ID<#H7{U=JKONc3&nH+PQ^m^INYI6g|i9{fxfrasIa`dj+HZYd?DTb5rftr8&Q`nZ|YJ z!?dz;1ZkFQOVDbq^zUciA2;+UP#)BI-q&sVU+3zpP-iaQAA9ir5gE~R?We!r57Znk zsq3c}-xcG3MP&NlC+F)p4xyUoc^4XgAyRbwtnRX4Ju0V%+d~r z>WWhm))ZuURI3&|MME~3ikV;)3zEnvm*k^ADp!LP(4ZA?Op0T*0|0q&ZP1L=g8$cr z0s@*8g8PSX48U!p=o*dm-K3Lk?^4^aUAAR)dx^J?l-(J-)nKqLl1%Awwluv-KImtzD7rApNi?Lk0ewm7bHinr9y zBTsyv)P-fk z86QZ7SY@TjFsil|P*D@%Qp!&Utb?hLiYum-l3{!XQc%xoRh1oxs$o)8z_8BvdEXse z#n14$-aRBlorzV{n?V41$7H3$7hD)6L1LR`ySrI;1y58=1f>(QI<2ix&jJ7zy`P;W z>X-F}aq zzCL)KDpe{B&#OY|`m6TO$3Oh3VbGlPdY!L>$nu}F;e0ICG;LK?7i;GEvf8&Km2Nu1?9P~4A($HaXxYEb9X@h=pQn1=dmgS_pKrofd3@Y8c%^y;VEBDr8RSCgfoRE8 zf?0&!!L_@5-k4wkb17k*VsX zBazZw_4)BUeP|r%=5Tj$w3F}ofkmCzFnF)Q^ub22Euk?hG0s}jPc3;8yHGCFoeP|$ zrH?!=s1k*ixs05_iHikbKuS3{I3RHT2PS~8NJmSnIK0+q?w@>n_9s8d5Z9Rhbr(1# zL6Hz!m__vK>tzSLsr`4IU){Y&B9jZmSU?cn_X5LX)J8EfIXlVKMhPtP^UKLl1{xyuE^Z<&zfX=sZ3PV+qN0bB8SGLq z&tEnN)bV}49*C`h2YmRRSNz#E1h^T2rK6$AaMJNJ6L=3DjkO=7pf~Z8L5c?|2c3kX z_J7%OKM?v|8Vsj2m|}jE&Bp{suTZ!6y?*FxTpL**{~sFL{3|ox z9~~b4+V-YQj%x&3M-?homcFHj9LDodhT_Q>lS*9LM&P2Et~y6c$_&jlY`|oAN`IF0 zefAt|Km;H_WItaI;_p~?RJIK>;#SinAS2<55Zm zwe8wKVc>JH2Wxh2HXKw#XQAmNFfCa4V6FH#IUzAojq8DLCNr6LHOhm-1Brv41?BwC zO49|{>nEnnOnV^(=hhz)S>A`x^+8fc7JQOv=L#X}%P}DF4DIlihxTaM;-3V-9o%!Q z2Db_kJ*=?)EowaDrZw6!F!AxbarZepoz0U@V)}H&u=ehBZr?1@v4kKl zbYC@ZTz9?beQpNTV(Ejg_IydzKZTH8`NzPG90wvLBG5j&>t0`LSGHs~P*U1!ELN{m ztqh7bEp5_Oziq!|CO4vYZ|QE+ zJ+%%?l_FJ~V-#ty?Zh>S2)a;EcS9_ADHhdm)f@-S5{M{?*rd2gMt?F6^@>-4sQcr_ z#Repk%Lh#`G2;~sAW91Zs0L+(M>&o^4e%5caGTOV+#Z;|^1oC=64`S?D(e6{%NYDz%vXj})AHTnp8t$HU0EdRA zVxnM@X=&JRu%$M1W_;eZM54XA)YBx`B=Q1F-D;)deU+|;M(Gx=l8ec2q*#`Cwcd1p z)X~^?J)w~n*jZoyr{1zaq@s--zK#a}ZvL%OD@_kkjnv{WvZ+k4QiRgGZ>u?%sk!JR znhX>~l*Bhf#c0ydG6W%$o0|yFrLFoQdb`%w7UKM`w-5cip8A!qFrzvJl~f$FK~d5u z5k(aML=h{M`e`uBxQsGsq7VUUSiy<6RH(01G7Tp@mY<)4Umo{fM1sHHlJ{~-VTvi} z`Mn+eI=)~`gn0oixIhI-{PO>K9m`USx93&0d|!_X9_O%))mI4vk_z2-!`Q1pp~d9` zAT}XmF>MaO!Yd`L0oB?byKqvlPxTh%b%4EBQI@^NP_bw>*z#Ag4#n6MSf}<7fX=;h zLjH_^n=~;KWZl2*Y81rgSOsmTz+*%KvO!AyPIBG-(h4eD=@7f_f8CU>XDlB?f)P{S z^_j)I#&7%7`ki!NVpL)!H1;i zx9#h->qnukCUVJ_(plAOYDr@EtQ~d)qGa7Ugy`Cq(x)gmseYp6C z*&3owCjY6@9=xvGKlnax@dFbN3ygBBU`;47byZ1WDCH%IWc1iKdRNM5O0u+Kfr{)F z?Oo+rLNHOw{TYhu6$qXlB6}71IB~GRJgH!9gAb2Mz<@Z=AM*m2a5x+Qg(!40fW(*f z$OR(v2sDiSlanweLOE~0UO<3~<+4%^d58h`mmvV?7wgtz9QU<>nGF#0loRdd@4kJk zL=ixap-EP7AM`-TUT2vm<{x{z*3FgDO6)*`4aXM7W4-Ivs=umj7=kNtVYcyP(~-Rf z+?)E7KuA<@?=aiX9gW&M-1qCc+X&1KRo7KO4JjDHq77(?R@+H1ige2L?Ef;^+@ zKukb*k9n2FvX0mjl1PIIiuZ^Ao~t4P9vi`t2i{-v3kT{c4MJS&`CZ#O-0Qo^tPo1O z6>>kji(;fmnpNOQ%`@2yMN**FY{~g1a8g0$6q|BCNLDFM-ib|zSI_HgkWDAu_D_l}AN+m1?%>{oBValTBMZ!PP zXsD#HC}7HGEBre|RTa-K$1mmm9HX>2n3K}Soc+*RqzU-1bfZ$RjM(ITU8 z9iV%Q(>=lI&e`&wFQQl->A~>Aj(uli)P-qbbqLYiQ;+Mcs7a?3h^SFk*0C-#M(nES zi2tjj__-haeIKZlw5aO7mlYcAeZtRQ$J4?*OuI(gM^Pa$R8EXUj#fXx43`>fCnYFT z)?83yr#0qMuqLRP;2 z9yWxe!`O$_2liHHPZh$V;s`eeblmmx$wa9MYa)$_xmDnC@_m&Kv=>yg#CIs=G1^)`(?X}}1>3ed0)#{sM7=GpAz{v(422AyK zyg=`|p&5P7qBca@FW0d)KDiqDjN7+C8`WM`E4?X2gC{dJPLmzhHyqQ`NnukXai_ES zyx+0U>8bPg^dpa`9gLG~wm^qmR>jV?4;>bAi&TmfgDssiG3U+-L$A?c^|Cu|d``R< zw}rB^k}BndonsPN?Cd+w#K(aPAJ}u?V1rCa@p7DK(Cg8-QN%ki;NEUkyM~mWKF}_& z(~_R^ZGxN^BcYSq8PrwIJlZa&NgDorer2T&{q?mn8E;gL6>mSiZe6Cm*!UO?^vUs91VV)!dKcFKECph9 zOaY1zRSFnI@g&4>@{q+pt}j=P|53#1tAb)TQp?nnoyO@Efe`@-i+#sz<#HuCp;i#H z$b`1`JhucJPf1z=_EdA-I=&7?DdQMQr7d-XnrX}$_GA^oqE0Hy2Yv*osTF#CaoJ57 zR1g*Eu}WDogOGa9K^w)4DUwWKMBj2UtA?)giBAsT#MiF(&i`Qm%yGV&`W^TCRzlsj zkc<}iCDnWEli*ZA=~QXi9UDt?+c<|G&DkMx5D$DFU}TtEP)n!B_8prvL5Z(66F&&o zHeZ5Snym57OuyEmQepHsTxu}FULgf?{n@vCDZ{pg6Q*taZ44pIZi$+lzid;JC`41EuWS%|oYYj!#J@UHPWUoo-%3G9GaLyz zhZNxaD}eb+wUK(3YN#dko5tilo3>wT(d^-yEu4iLd!1s?J>YEJ1gtl-vJrMnf!opG z*(U3M!N$gIvU#iYA1J!6_T-TmLJHAbJ5;GbRW!ZFin)QpgvRxI5UoV_s~$R*<|+Vh ztk)w0y`)eq>>&~}RCJ=&r$@kQr!ChA7mAkd50{FNq)s(6VB<-#wLN=Z`&59M^38&0 z5eU?@#E`)}Kd=3d;5cw2X1zxK;6LeI&d~LgPzSFq%X%ja%ppQ|hq9EEM@c$JktEwZ zw*sEG1f?jHrK8b2l<7)?*+e`Z3gbtD<6R`_{V|LOOvaOwnG>AezW(~wf0r*O7Z&Go zG=BX4cH3V$%YFQNsqoj|`Id7POU+^~UY!|UYLKEi#rh|j^Ga42PiZKDi-l%o=`BV_}JhN zga8zkLhNH6jwIHFo;32s@s62)CP`!ez*6u zA~1E`b#L@G_OU_FC&#X*^c$?tJHM3TBmFZ8mR=s16gsBK{4Xh`pV<6T#{x&=n7wb> zUo8JD-b+WuQ7;JL1#=J}2%C(f=nFK47}$I=(-bnMS4|0a8AMVVhR_&8{I+irjLn$Z zF>*A42_|$`8yu!|(r+uVaU^hQSFXs4v)ZeiJ$8;w?bY595W zkNtLcvdhXf@8izt`U5@Vb*0Kp*N5QKY#B2=zVGMR!Sr@vDfzWpW!iAL2VZQhc5U}{ z>!V0eHa&McxRv|MUydGML){v=_e3&M))X(Rg^#NeLOS?Dp8vwEPe)!6(gPGLIq+sF z5RM?43>|#U4M{?U=;?*k+MptzalLR941j^Np1Tbe{=r+%y@Ct`$TI|a2q16_)TKSe!i-hC|Hjf?!)v|MV9*TmcPse8OqNYcmZhCeoqVfQQWCaw+sBHZ z^~Gs|p1Y!otqrKKbcT&P%jB?(p9YCugU?f9@rZmb zUx(FI*wO{tx7mqQa8Y~OEe#AeP|zqKSt`AXmJ26@$NI+utXQyN;*t3|1HP|| zwfOWQ0vaPtOv|C3lqKKKhx`ZlwEn`fu5{tw!mcO$p62q7z0((n;8ON0ZvCxi{a-1U z;FEeML$Qe0&y(d^%Ufj<@ZhIiP)~o*f1oy1%O`vqr|a+us1~sf88VR8x^%$i`J z5p#$|EKw&@Y|NQPAyX~DQ6dM3O+l%asCJhSJOq}M7TAL&U{;w89Uwi$nV<;)NaE5j z&}Bf&)2Ee7f_AFazZ%20lFn}6m)>g>770Bi(1rC^t7-^np*0LzmWwzQ9Lg}jgbV)8 z1d&0Srb~_M^FV&*QU>(Tk5dpMRqA*XMb3S*x`cv2{i&FaPmvBCc@<}ydLv2FB^L9RD+{n;qCA5tQYJOBvW_RYVtW8em{K!Ck=E=WAM0N zmdlgwLc&1-$~~W63?d2QYX1eoVkonVFgn2EBp8;*ey^$vst7 z*rLymaCqLdRcASuMw$r-E^eVSOX0LDm0BYkndH;2@zG6VsZHM_e2-Ax6MUOnfJper zLL*5OimoGxFI^2rKEC4GHZni?uJKi9MSLt4OFFSx#f1rY0#q8M z*@n9cy+|@;wt&yAlaXd16aBHA&_baUvOh@ueJn(!KD{tfC75r03K@D-!2XFsE5c+v zyR__6(l@JSg&~D24YH>NZ&h7o8y`p8yT2-&yDO)LX&eP zMvp#_;LWAQr4PO<0>qe51q}O+kG}2FhQTmkG*CIP(9F1uOnXoF%EEx~(BX8QOg+0& ziqjXhrn6uiXE`?H%i7N6^GNM6B{o4zzIsQFbUkgW0vm?RM53pm%gI*@d5rKi7El^ z_9nHfvIT9-L-%Ed?L13Alu`sXunq6ohchk*EP*7E@eHUef)mChEIjjd^oUOgn9F&c z4h$kg&6WFTG$z293YsLYXJmYj%$-hAr|k5beF8ZAZ+tMpuP*IXD4~WSVpW0$d8qq! z7)iP|ciJ?^!O@W{;vu+nBC=kRDA2{NNHMpuQO?rRw$w0Iz`)uVcxZ4C#Hl1VSB{-6 z41_9FL_aNtC_plzq%?GGZhRamY34**Tiey$y~W)nBP*jHIYaxmP`3g_1&`He^Rt`8 ziefQ=c%ekpMEn!>Y8*MVA_}FP@K(U+EUBM?eBcYJV{HcumPpMN&^W^FU`v5XL7Cfw}#N5j~5fdqR zO4-p>-?HC$;s_@fNr0YEPCW; zJh<+XA!l9&k40}9wkNp0CZ;dZ?``IuKTBPZf0us5ON6!^t|%C~f$Nj{B&Z_*vLa`k zkiJ;l6TNVKHqxk-eYPD_xbT}HG78!1NA2X0tF!ZieH4b}yz6Zf?3zM08AI~O@FE+9 zUE!f(A!F$gj-XKGNAEjEO9mK9B)=y)I^Vhd+@M`SS(IjIaI+eie3rR@+St)CfO9rn0b+Gf+K~$O`nyfa6~j&=&lo8m8t^1a1n#pl zhn&VSvP$y-laS!}VYXA*r$WCcTOuNIvewqJPGxiZf*_p6wG>W=lB}XySP9+qsU^v& zsR>W~WpNw>8>8)J?%lU2!|lZh;bD2l-8Yu%XJ;)-o4W2_7K-J+>OK8wOiiG-gru0B zp~5{o<_qkvkd8X9j3g2;@{6snT}--~$5%&i=L@ zXb)m;Bna3xRwpLhX0G?%Ox5-{giwYZPc9$7T@1kX4utRu&@v5$3PEy+E3L&+dJYz% zVK#sKko9@XfXyFk4Y4+)Og0fg!_WgJ>~_lWkW3X0p%JG{xL6hgO0+4>Hdl0dOad7% zp^ti7PGwf4+TIPh?zes!I2&2!GwD5@O;%4iq-Dmz77v`0E1cWAcCaM{vkdZ43PU(c z8@?O0wF^du!y$8ACB`KZmvX1mPN##)Gg^Z^th-WMC(XwdC*Y3fpAu!12!gyL{iXm4 zF?de!E)I@@<;CWB3${bkQ9}kNA)%n1gM;_OSN>v!z+vdM5^vk5fkDl39TZr;?M7XG zVnt>=vB$@|%7H$Tj#^h-OyCYK!w10TnIVT=>((tHhdSulj?rRds~f%w=|Is+MOp%M zDi5^b7+pBWFtFhZDX0C(Ww+`HS?=l9+SI@cV4EV-VAw}WTu#_a4Q8;nCPTbm;$rke z!pehYH8smmuqbB8zHB#NU5 zow5!0mz{NEUB;aWZ0^YXA)$u&~R3 zEpUzKWRJ&YZAst57onnv$m}sC#Vpa9i4x!0NX5X$TX?>`G$bbH1=yTqok68IYwz>$ zNrKYXKx|2B?o;Lh?9lW{G{^3r|S*W@yoGul&+UBR3wrz@v%?}KHXSxJCtI^4cd(; zUq^_3NpL94M0*1Ivl>gh{yVyn%br^nE*Tb}pJ=|2Iu@dT-=}0f`gf#(u?<#CBm$~x z+A)y0T^S%ae1}IxIH3%0-IJ(anKuQceIP>xkwc$mPUHTQJ1ymiJJX8Hl&@-XLj%#> z4~AT^)G*VrO|Px_D$Boq3ecAyu}_%G_ZuuDY5M-xO1eKa^T<`Qg^&ajXt5flmv7TG zGJSxJ(Xh~wH>qA(*jP@1+WFAgwZ@rpF-f5ih|e~(sVV&+y~AV-EOAQB zKq~W(-vHF!S{QA{z+TJ1s(^Q30ua5yP5eS+us*AQ%%1>@3^bC_QM_|NzB zDD*rmbuKS$T})*5f36^s*9&Ga({K}vMF%_PB8VBUQxhiCCKQA>X*arN>7K+VsxB-F zc{_+B1}j?hM!~aL?}~U^^oCUcNNYCi;kjK-z{no|kjSdZ2ZBdP(H$R@VHfoJVLxxQ z1qpIW+{W?w$45r$EzoWdS?MQRB{=R_XA$HaFV0xeL9!gf*=SA|8V97Pc^oWuasWX@ zc(Np$x_xeX%ulgIWS!waY^=@*=52%D(LPx6gE|Ad4|Z{ULX1~-S zq*bq~O^h|b0UoQ)a|tfsz<2`#lcW?Nxz(XH?Z+PoWxY^QMkrq|zEZjMW!x5zXF+5#8(#;@buJQR#vj2Y zD#NU(Jw)j=+M)41JDfUv5pl3`%;#idC`!m83Sv(70F}7_B_*ZEu-I7iK^>jz<1(=M zN0)_4ZTZ6aEx5}t=G3PSiQ|WI%%2+`fdKbUq`U^IEb%tg$^(-iRwe%(dA(JXlFGYunK*Q84lan$ zL1;6OCiHLH=M+s)M}8%%813&9O+ix}`HFV8tI_X{sl7ozZiZCerT%fzpZfL`)$Hw~Yk)}J?X zo-Z!ax&j|p)gR9XNsb0z2Quq`_?QGjW*}Q>&eyUFJ4j2(xD-5ir_cupI1RIk|Kz;* zVGDMf%Jx=!7>YPQ4Slt1K+MgMz%6OBI_lLC;a}6T;9{UO3}*vf>lifZ8{Fs33jWu( zh0G7On&Y~$y2gCCC^=UYGPvJ5MG|s7lr+{F-pvWceFx!_X}+6HAc>S!PwBq0(Gh0! z9Nj~ujsF(ICIJllh^-Gjg07m0wHl-jd5Wb>u^&U*Fx?9 z1mqLR2OL!&H0!dj$7GSdh(b(@mzuJyT3X4n;~b4bRzh)$)AAlSpIz|F5t=51fh$Z+ zzd-tgN04d9yLc^s-g+fL88tSq17dZ*b%w{Cx&^bvw%qcL{-~+|b@6?hFHk~l=mvKL z2xn<{Ckl1MOJasj>ecO~<~I4&*x71G>gn6>w^$ymJmLJe!*lfWd2_W|a+!zu-#2IV zJsrn+<(56f5S)1ZsItT{6BF!S4L^U-GM5Tcneor=M~2>E!%0hoeIJTQ`$iGSE#OmJ z2jfrMEGPZewZ;$Nd9l=wAb}%_w(eMEWmAdo;+;W>Ml|f)yMpUs8K}Mp5^6dww_%D> zZrxzP{mx#=t<3z&4z|-xSNJdVHR^3;+TMcB4a#2BKZbSv?PZe#E#(Fk6jq%`KWC-+ zBRKn*fG9yap;5wZqR8%b6sTtXANKRsj2!vp8%MwdxQ4z$e-%^dg4h6wXkN$CDTe1p zt%FfM?#e)=UFGpVRfnFlkWz%`py@jFN9lZyK9u{YiUdAo-Tr>Hm%@EB_(0Xy%~;&F zh%?2zfzf<=FoeB9=0C=VouyqSVDsU%$Ktl!x66^rVIs0+3o;JFZ;yXBx%tp%9z3&{ zrVZCNKzG@}QVONFru$X2rI@|lJdlCnWK~216K%g&bh&atuOdjNw1yI3d{<(iuh>>R zqQf7S(XgZJMkWk1${LgUqVRXc_4gmY^Ng-uV6ZF#$yOutN3`wTkOb*| ze@&G_^gnxsktP1VKJ3g9>g2HL2TEe-EX^ zyTZWc+TEzP)Z|%?{;j1KWC=&+QR0u2cn+jAqy&Nu86?M#*8W|vU)HXPYu2FKUCwm>d(qxO z00!r(b%_JFvTGMofquI-FT;S1go)K|MdcIw1UgAwxruD#rZ6+kr7wE@LuC?R8NUZji`(p1^@x<`rB^|#S09AX>gM_AQ zCvkma@XT9tQkOn;)_JVUzhp_-UE4sSbju{efZUAa;e*3RKDE1~NxkC+~ z<+x2s{UWfSactVKmOxft5;{_ut$%#V=R8`q4IiIO7DBN^TdQIZc)`VIw+L7AosY(* z0I}8W=mNBK8o1g6hLAS}Vvo=~ z`eS=HLRMXT>%#0uz{$B#X|MvWEL*WUkh{k8s=C_owuOB(mOyrCtEIDA?o1GxWhN{v(Y@w|C zuGs=89e&)K1CvogS!EBc-wllm0Gs)*mk%DywiOGS>!D%7@WYll42|8V*2$bpY$8i3 zS$l~Kt8WN^8sdpC=($M;eNrE-yOUDAk1bm{R7(>~I8foZfnwRou#e`71LFq3_M*2Q ziA>6JXlQNd09mq?Nc>u=74xNBGXDOw28kdf{q)wY50H@ac)KL{{OEIgl*hCk)-J(= zmuTS(VWs*-+s*EX`!J~phTnJz?X&NYiS~4YOU8xG(pN;T(bb~QD(f03*wr@%jdmkP z0Z>^-d-@)-ZqN_0yM1rn;&Oti&PwgM;uy-lP$7MlGY^XQMOqT7TFvNa5|gzQF4Ue{ z098*0bXAI6n|DseqDI(OJ z$lFA$A#Kib4VCzlEpPAfc0Ivi6?=6khn@AFpz2=`yD45jTB2}mQ)W2?J`7velc3F2 z(hMdr;vFd|6MBFXn$V~M&G|Rk#*(3RaTj|d$<+Pd84xsK*N)uV?LKCE=`wt+VLYDh zd_^v)FwVx%#U-(SLPqqJQh{cL*M#YnYCB!vjf2VJP$h3tY-=h3X2uE_sh^`tpGx_X z?elCj^sDvLqEQ=hrTu8escdqHeH~E#<>++3rJt+Q9=p>=Hd*zbOFjj$MxDM3l0uCW zmN3yO5&1DT38QX4;}&}!1G$<>2Gsa`ww0oRa(Hyle)i3LIpq^Hcie~TrV^| zL^A{}J}~+v`QV5?$I}`EuRUiOF~2^o)`0-(vap^yX_>P`Ds#2;v*i9JR=83eKyz%@ zJ8x`ONZHGLPY#i#yW_v%wAZqGh8`Krhgt$em-gKdliQ2s?mB^ncAYOrXgoM(n(u*K z6{-Z4nFWE`BX~xgg|8Uf>V%g@WOb6yH5m36UeC}s^SWv%{Qj;c04pnzZLK3~OR4WG zCEELJfL5^j+~ntUBJz;rK@ChQUur>?#{@AF3cLOJ6rAM|Xr%ir-Lv!fIwwtxXU*j=x*T{WoV^OmyeX}JUu^cm>b~^)ak+DI4*psWI z@T3V9fZv;+VWLi_QLVofqAV^?i=yf;GP+m7`gpUA!}&rjjW0X?hZ^wfB$N`6UR@S4 zROtxwH~9ua_96h)D-V?~@uArQ?$v)j1XiroKi-bk&8c`)n`1Yp`7|E#rCNlSfuGZE ziijd6%}fYgemTE|Xl7dkp^~pc3H2mg`k%yT>|?K;<{bzBhz;oDi9Tzjv&ninM%0|x zS|N0Vq@T3FUsccAF*huyU%jhBU6x&%>i)YU5(qpwPpY+gCQGY<(H~CUQyWif&20^% zUsc@ZR)+@74JR^8&O2j8T;*4Yw(Eom&-FfU!=12BuKB6E;rg%z*!vJ*hzY;|LwLHb{RUr`vUVZh40J8UrbWPzsEcM({ zpQK~oxtfGA%yqK&+il*D>p-zAN0lC{7}ykZs&W07)=<-I z$kci$Gv#{Mz|2f`a-;Ihi{PV#`&bCdpVKlosF5Ne>{ip_BlFWPdM(%b^Y8W8SZpSV ztf}=tb?+X63?y**p~2SFZd0POj(?S4S>4nvm2qGYJ1TV|LMIq!XXP8Op9@&3BzJ#zuZ3q!%^bNJaXxe+nz%kHTIjaqm_u+@Z)nbUO!{Kim!rG>hj z{IHm8+2eS2JkoFsPJK~)v+MVc#DX`ki#}g`9z;z{lC9E_x=PDrtbl;dhvG2{?`{8+ zE)O@kG%ax1bgvk@7yiJ-koJRJRHtCz%GRhky{wPRNI9USfx)>EW~}c?q&89Y=`B%t zf`DG*d%mZ7a(@AnWzI<}hCqLEUS`ECg22{Vy>5>nY9*UEc3ia#0*ED)s_){^A}1e* z2^FuZcXzuAZ;aORsdQ(0;P`Hbb;O>!ES-+m+L2xMyf)OmHM|~YmQ2QL89%Gh?F@1c z*B9!1QKrY^0oFWQ6DE2T-Qs7+!IFQ8(kZ7zQjNJm;f+Ct4Jns+@!Akeeyz!*N#%b$ z`ixZtKL37X$_U)!9#DzPkfK*yQaI4N(#Yj}5VGo^7SIXzIDtWu?JTj>?&Hz)0AV95 zOJ3%OFK3Mue7ofqDixP!VK;35Q1whjk8OH;WE)$x$>Nvz9K5x7~|df=2am9316;6)J?jg3AZ!i7FDoXLd3#1+ozCRk?*`pqOTvQ^56Y zwe^euH3n@s0jP<0t!URw0VrE8Tm3X-U}k4GJk-ELL9(jfkYhpsk^I4x74_kkz|hdJ z&y>c*$Ha}5^lwrV5Xgm=!ORU^v^(M&@bY+9wcITj=tGWn1lp!8zlfl;C%ffnY3L~CSKDI@bT>)m`U;vX35a(X z7s`4r7d{>CacK3plK9;jl+C&EmXL)k&4b0@Wpr2r9=k)T znC{9>3lfQw{S__FO1VMB=`J+Np_Agz8hsq^zs?^xf<_Bt7K1UGRi_gt0>Lk_Hw25I zJu?}Mn-pYut4q&Li_?DzW8$bGG#;C_%VKhkP;$6I`-ki*mK}%yo{7!7jZbd=t@ckU zOSq|zUq$jezI%8~eDJ6Ui(6u>7V4!Z^20-&|1(MDAzveB>Ls4quhwvR zwT)ju?c9|WB~E547LqEXnHtdOpT-N0ZolaG@j*yhc7A%)xOopYtsP{(y^QNeK;;?p zv@fAZ4e}I@PjuOPt1Ts~5tP}4y_7W2n!9E#ju+k&R8=|hB?`wqz&IBA$Dx2H_|rDq zc*>P)b&*!?72EZPV`av5SpSrzGTbR6E*H_;{e15wZTfj6H8D$b{Y_~I0Jfz5#J2dX z3|3(14=fB)vkpG|ZQ1Q3DkLNYlJR_=ZDe`x_#l*o!%W6N`GuyEk_YPUX}#4(zj|v^ zG=9#Y`I%nrYvnK(@AMT&{c4_;TZaGQ_2wr)UR`>OM^>4yHiOzj*8Cr&uA(RQ!E`(X z{SuD8#nSzgss5crHd|>dU(#Sm5q1SuaOWIN&GVG7O zZ|+9|NRfhZX8oc%>Op(1M+DjMdHh4hOjXYFovZ#xc!|;8EqC~U3|7EMlS9TL2W?_? zEC&a=Kcwzibr=nlh_67HW^tZRQc}2j8TU3o2-@jyAd<9yI*qu?PH1UuU4|BmgE5ZT zyt_=}TdY)RWY?2GaOUqpuE%Q_)?pm-$1dbW8W$m2Vde{u;CEQ=s5Ni-$;3G#^7|vk z;76emTm-YtnBvLapACb5u|4NiuOY%t*eUuC3bw=?s=e405RX~+7at}dj;U7Z6d zR5^Lp2rwl%*yTT;R?mK-*$A=UxK8oFm-luFrJok4j4hA2PDt{QE|2wiq`U>rK_RA* z%gHu^k`$XcZd&T99IK(OpUj4$*9?s6z6t^R7EPaq0a}nNB{Q?MJk?aov_z;jx381h zO4VjJ^H|BXk&ME|e!Z0?UHnxS>`(lIX@gCVg%|y|mo(RW#TK%0=>l|GJaMBZh$ivZ zvQWR?2YfzU-86Y}*zK5CRH;+14!474tNt9|GavBT9a0Z`q0{cl;g_qMUIF zjZ#ZIc%B!6%*dW1WW(DKxBhl*hX%tx%N#|2BZES@1yRX)RlLIfhGq(@RRy7w&r%j> zKZ=i;@BfzlLMDdFbO#B0sDk8I$N9a5^1q$yN$lF9pq0bZzrf5b;7PukMa5@=!(j8P z+UlKcGKez5A9LtroS~Y~x{VBJbdpopKVoo4T{&p}WpfllY!}NDcXMKZ+)G`3NlpOd ze#4!PJOaNtOntR$^!K~6*ol>Wutu{tk= zvgbBp{g2ekx&xFS>&K7@pTHmvJ=q{zv_KWx zruADXkd;8PrFIaZYg8&y3l6Dk%LdU53lvriF2+L)V0bp@9(sdVPd%b2=r8f(`cSYZ zZM?o;vqNTm{zn-ipbZtuX4aIMenV)Y`vlm{(x+zpZ9~*x<58eT?j^;ZmcZI?reJ^} zd$dFhVjz@-;RiSI6zvxI3r}iHahBtPLm0gnVqGi7qyB|kNPs*$F`KgPJ9GM%D5GDy zwKp{-TZiA)nKU^Vn>wc&*umz$i~MuID>K=7dq9e)yCBCos_X&sU5UJzW_p#H*u2Hw{G>MvRWL`doLre>?x6i$=LD@&VTB#f@sUFi1%?JLq z!GOT~8Ve9tBDb$*%SUq|BsJ2Xu*HTgF1S%y{hD_%MSc#CI=ifS7auaqa6Q!5=j?NS z(OTKo5G-PZ;EZbw?-E->%H;>VnKSJZZemzybbu)l5-S@w+nG5-NS6l-btXU!S8;x-ddP@BZ z#Qb2eJmUOO>vQ#BI6rGw1nc?!qx0zFWYVhPHJOy|n;jwh*lD)#njbT-%@tjQkTME~ z4L4@nWg9h80?;Lp1TzUv;%__{VqeG8L(@XF&NuNJ`L0m%TYdO9Sl|Y3)lk7UA3Lu$ zDg=Qvw8R0LdGARm6jI5ZsbrGzj&AH}nsYWUoG5?;P=5VMHCN_@h;EvdQ5nTS*0%gZ z%Blx1I>kTU_$Fey0lvi^bRCV>UmK#d{!G88QjX#i7K2{OL;0(ZiW$B6Cw(Lo}XueU3+b=lWK`Km$*vn_@)1GSl3T^_n0; z12Y8d(1mQ}nmFOumkI|#VSo#_gs`-=)CDxFL1tu>k)avVd-ML>qXVoA$bukPQ}IS6 z7-)9T`v`V@k2n5Z01|Kr8AElfhM3gSy4SudpmyNhT}O?kDuTa83Beh|=40BmA@A|a z=6f$~rsfpej8Tcxr4B@SK4zab-AqF>!k)lSKE(zpqKxK%3F)s zZ>hsJ@%2WoIDMv1WCG=lfA{E+)?%B|^M;F%byo&6V@HBpH$fMpSie7%Yj#6MX+WVv zy#Fu%IM*g^5cPwokpo(F93pDT+PI{|Fw-mWwtd(QIL8NfUe#rTC^cVDj^4=;uYIeF zqw29ptJKD9pe#U1aebYb@@k|4A=%$ZuO8O9bOCeA4NTOscp8r0B`6UXU*EZWd`$@;4iqT&3&<6D1NYYw{8AYW3%TWf%S* z)%T%(Yz$f*OB068hBjofPMUqkpRlD^0V8qQ4@Y&HqpE>1R5mxYB{RhIH(@7Q27Ilg zI*g9K2;-83%2`K0zy8Y}1_HXPe)QN8W9I6+d(bwoXrE+x3YPr}yVBgQX@g$?}LC3Y@HlKT6JKIXEO?SSHtp zFY9t1%%rn;i7&z$GQSYWz}r5AGPs!a@^{s98avI7 zt3DtE8rWU&*__`)Nw_7K)+w!Mor;2ilk6#-<=7SlpuT*gUT-O{e4&q7W7k-?WhKNp ztV;R&?L}3E4lhyI4rI_Z1k2c(t=BIDX0zJ7IzG9@I`k(e{Yte>UGgcKsE?*1Q6CWq zK48Pmysk)Fe)wXkZM1z=4R}O;J6{5K-q(+0`(P$dTiE5~UqzzS`#Cueh%8lE zzE~hv5;qr5a!FFMU-5+e%8pp1eJxS(iaQd5lU2oFaoLR5yQk zKe#~b@gMmvWZ$d6Hr`sPU2Rnm8(G!^mY*P3B(}(BkliG=-UT z{aHNaKq}Sn##s4`;(>HBz~!UkLgSVzK_;)6)oW0;GcWx_n|%MqpI9$+jzx8)tBRV; zs^bFF{fn_FzY~`SmWc9~Nw{}Ni`+WkcXEi7#%ZUIIHmvWEV{9OnYT}#uhzfxMD*5G zX=uSmPnLWM&>H&39drq{2b}+gYh>{LiP^~(f!8ai?Ar#!9IZwFBm!|F`+367_ve3Z&^erqB$ zsZ6fx*q9I|8F=aK;B&$_pGF(m%qN*?!IyM?!f?KmH#a`VkLC_ZGgSt~NwrC7spZ<+ zx@M!ERN_jfQ^u9D3`uKYM`B2T))sOW0s##}I^ZpZJb{3~46puy1)`_?u`&%K1M*;x zB!1`Zg?e~6p_r4LZ{9o^G{~ ztBnjbN!LQXMO=h#WdH)1-|WfH{X*X(W}H8jtH;YxZF9kbgM^AT;pEu~;spZV#rceg){(~FKk;6^K~el^VhsGsyt zfpG3QOpwD3Nr*l?yT~2IjWV8&I%BQBB&nFOQumb#O6PPksM7CGEhPrd!ylFI&n?6U+2`9@#apzbID$|`>;|AkomN#t~MSct1tfH zgEqCR$#6Q0(m`)_1P@4W%3R(DB}XcXTvSbUo`P3aK-eEp#PA7+|%;K-sh4sxme_||%6KBhgMXx3h7!OMb9 zOSNiaec66M-y6}Yr(skMLFR(Yb8@KsMq-tW`^4bEr*8YnZ(`7-9|1Sl)kY}22*BGfE}Qj)_Zunpd?%!owy+kuH^;Q&tg-k#$%6Wy5d*F1zd*Y#n~`*ewjNS3F?E$ zaIIMG-Flo1hha8`BxPi6f>=I777}T>e1kI6-0vfhlr^O*RMiu~ndc|;#*47nwzm}P zmfhKVlSNHt%LMyotm`X3UwO##QXoT1YDj7pA$$mtT5AID z{5QBcc`FgbY2pja7Q1pzLb)wp)LmaTHxPD%%=ZCCHZ0D&1o;Bw%GbI`{oshDkwn zK2Uef?l|k%=ex@Hr-molsAtv@m6y0?gC4uzAwt@Uid;CN3J@1*(ZW7|JA1#rnL zjnvjFe)h|~?(7MLwwENVn86#GYo*Y5^HO{~y$c zPv<$noaFMBggcxoJP2eCq9>ln8KZUEUiPYj+T1FZ2>bwYCB*5U5q`&NDIcl+IYuGy z@A|S`JfNN-Zt#!DN1KFQz0M6^Hv)FFqHgb*$x+!82l*D(-i?33JgZyMdcpsf{Rdq8pIhu0Pfm>f+h>e&1ch<+qJJaO;! z!^71IsQ~NboTcrIdkhpIj&J*zNL3?g&L8vs`7NizC%yYsu7BGRbm>E^_qAQW_oiZx zQGI0ASTQ75cQL$)beG0pUEBdmcs85meP7h^+Zn-fH%6d)mHmapgAeENf`MDV^_Sg% zq()*z!~jo8^E`Vh$2 zl!DIPA=C$Dfw~fQ+&$Mge!Qca?dZE(%9BU{Gv3U9$25YSX=}=3NE-}N#z;T478PAu zSP`r)oBFuF_@lt5aW$@lPu}yJS!eaxgRtfgBr6hdcGM;TH|Dw;o7+8dr*FxVmohpyoBOvSjbSSJcyu( zz!>xUKrgx@WRgM06Ds#U-Rf_68efAK~U(2F^%X#CExIpKtURC^Gw2H=d!3eaiCKW4& zBf6DNW$JSR{)y_%HwO5u53ue*&6XIdpK@^GJM*&LjSI+zD*orm6TzAlDeIjyM~swb zS_!v#Gh@^51de`BXT6St(HW^$H{c@)w)pv=eOpOup=T$kF zrSrB=qr?T(%Y^h@j%+OZs@%1V;*<-|(WC55ne_q`e3H8$*aoKNu6`a&|qFtP@2XE|ak7nqVcNV#UH4ipE z4ltzH@UoJ5Umh)rA6`FQPkn+riu8Z$h?~A-eJ~r}cde#`rrfxuCC|XCie$H>4V*Ch z`kPd3^)P``#AF6R&wGNLf++Lr1}iV*y;o;>7QF0wnRppv`VZv-0Rk}$SJU3Up8W%L ztzbs_9Q;Mc8R86}il) zbYNqr`{dzYJ-jToW$F*x(?$2%UT7p*uf32#LE*y)+fX~nLSLEcFBdSH2Fk0Y#Y;uQ ztn@%Q3#q?;{2_%;L&E7GCh!Z&cf?ZgaRckfP4pI0?Cdf2_NXG6Ef8g2V{G;ZO*Qc^ zrYL3}O6Jt@ENEiTZnBZO_e!q|T%osv#@CX*qW=2*OGm+XmnY(A7&TeX;H8mrFa}x( z8%T;4FA^EKMV!HEiHlYz_L-A(f3l|NaM&fNfQi4dw6yfT3>~63Np|Q?y~ma)Om6eN zPt8v1$o0wMLm7aQn~^pms&yU^$)xVWD7v}g$2WA0g*vueH8;fdp2IC z*}PI$4s{uxZUk^1 zi;+RhOS=z$1IzLk%?3r1N|N#aDgW=6Gq=8{GM9sr6^4DOQuBW_72R94Kj{l8L3Oj- z{ZT=C6d<9=V()rKO3oU-R~KY1be-r5p^71lT;X5x`ty7E;4a>mJxg!b0bHW(a~hSWm#dkskmL49S(>=xm; z7`D(9zFEMC^Vc9|tXn2R^i7vjG1S1nawBz+w^&9nPCoOlQ>VY-3+UcZvWdw*TqF?8 z7%j5UcOfnQ=DlIGFVw{GUH69+CGkM?R+=dnFtGu;h5A|JKa=@Dfezh*Cic?J-JRgS zfY2d$C*1qcxod(?jVA&GN&#l6T8N+^`0T0a%-)42f)S#bMV(2oF#zW9j*yO0r~+mG zsSlbM_11*}=9=TF9X4eYKe%pzp(?Yphy4i!K=3Opv6>J#cqfDcLCPvd6n*qnf!JnW$3$ZStSv=R-%vowdF_Ps(kBUXURWKLF6AQHTzM zAZv`;@=w|^K&V;_v>%=rkdb?8`sS4k7}5#P760u63!()Bx#u_RcZeN14G}I^i5!K1 zbTKiPeX#hicBlcj_^i{ zUq(J&9Qd)3!~~DAJhtwFZH8jQ$}J&~Mo-o@j~j|Mw}{o`FV#|l5LiJZ(8Phd7RX}q zvVs4JLLgpOw@cV!pSx$Qy>(U$`a`N}GVP4*fVi*}D@DYPI)wA_3l{>MYppxqCEMr~ zux`X3W<%cRT>HPrM<}P;Ba(i`OE&}GWoDI}Yf)Z6!kXfjn~?&-NhQ$n-US-9GIFFz za^nGDrFtbu+lA4UJd#ouVjN!n>JnD?%w-zjBI_XlqcN`J;QR4>X-gWugcNpTEE6#F zdP~MMtI>kmJWm8jS{prB&vnGAUfCOGRKMKy`gHZ2p5;VNmS8!NU80Tw7AIuGoTI{9 zG6?l}*q?auFQOb|(6dK6P{xc)+oYtrUQ&yc5S8W_WLx(IbH5bp|3RRg%f?liz|B|a zULuHue$@Cj1?0}zFK(yCWN565C9)O)*koCkmb2RE=%mI}(VJrKLh!d|hR?rm0v{^^ zx&GUQU~%B{B@^&5p`-4gX1k>K{6S+KKf&@tBlWYCQx+2f1Z?O~IJ;nwA>aif0Kra+ zqDDw0SlvVU2PEYWa`=?QL~$HS<<4WT_q^L*mi70-mihhFNciO~sxc{63#Pu^&wh7p zEj_F7KJ}%GdW?9QK!Ee3a)Na}A|4Cfxy{g9R`k7_6Xb}Ie`#97 zI8dbkXac0}^R!Vn6Mi zb2Q%5n>!P+uvdAx}w9@rPz(}<2sqc1M zqOnCJn8V~`xoZj2T2)-^_lLI!+dL{d{Qj-9Gj#GI%!(BP2A!nEY!JUFVR#Sl#{;ym z(Fz++%1=ZVxmJF)b+}%fJ`XN{Zam0fCBpGCnzibeeLb`NHdkdQI=kF246L?BWxL~4 zJ1jX#9tw*KeRVd_v_upEnQK@5rFfI%E0T+Fs6l|`aD9<>=q%nDvBS)AdBc7cqYukOaVDLI%>(mIPbR-}1vo3T3b=r)+9Hft2I5SY7iCLSTJGlMTTWrSh35>jYpnT*^y@pETF*wo$pZZHbr zr%%{7!%GIigOqRy19)xG;f94PP^P->mr;r@7O8yd7=}CLI3$-+^2f8p$BSe+lzvo) zA~M8T5dmbX`6A<)s;hR)Gb>>fgo}3BNMq!o;_xV<@W_-stTC6P?h>F!wl4N*UTRl* z09(*!sl)NDROvxUo@bsep;935^bcmo^EG~%hfS&;9XD8SN>qK`aO>`L+^`_V#m%7*Q@KT6^xV|)eO$;uu5V_$kz#@ zzCuVS$Nfy<+34*FPK0?U#-6wrtXtStxLUJ{e~vMC4lHiYvqgx`v3do1{|{n7oxi6D zM=5YVI z^Z3Ck8s;z)*$`5)KaysC8@?C7LJ)%;lj`n0ovdG?+5xdk$Wo_(7T8idt#A5K?rH4QlQOOiRKR7$D0Pc&wey|aJ! z_dogjNqb+3s?!WzRjp?))%+$OCf0yRO5;%F^eKpv08=zsr#4z=iX;0*~LnmMvhh`1PBRqz}nGkW_2T zOsXm`&Lp##Oq_mO)msh>$;E`gj&w9jk`KXSCSUVThf~W^KJXj@^##de`mEs%@A7ppj?H4Y1KQO3 zPJ(agsIz+MasD2Y-@tyE&qi)zr zOAXYXVKJf%xz!3J$*GeGC|0lR^7XCWy(b)iQO(nyEr5dkH@!(k+B!BcGz@~WDoiEX z-cymYKZ6j$2;`n$B|%P=W+kR)@L8Iw(VC8t7tIHfNnVvzRDTkl&R#9132mpN<+5vA zZfqZ2|JEDNzx3MP_4hZ=KVXt5cFkuWTx;8{t-a57uG|TYJp=*u%=q;Rth=teA7r)TVB&a~)61`OVAejdenxto+>C~gOVg=k-R;Qrk zel>@`4wjl@-;t&{L}L1gB1UYlbnU)EWDq2Zzh{=qNPjjxIms;*-FJ2S0KN zmR}C$&iBS%|NaEc$It@t>Zp8A8CB&LL|_AP54PJ63^Teu47()!DV0|P&v)(C>r&$x#nDx$)QKd$r z86`plM)A=Af%SJbD&$*HpsTzdCsdNKV{UbR7n5PW?s*;vrlyOFHU=|bq}2!kZG=qn zu?8RRe`l2LxP>$%$mGF}yxBQeZ=_+)ndX_(BIBgeBgURJhuRvEDU~v?Bc}>wK1 z|Fh8|I47Ofr*KA6_c+1EV1k)gt}%qDzh_Yi%@nI=F|8+i<+`_2)jHv@vxCn+XSBtW z&%XDe8Lb(khAJ>s8TcYoWe9C@;#QAJKr0DgC2qxz+UpT{t{t-MSI)_ z1)X0hzV;M!VHVx=2z6P0g~GW2^=onqiw_{doM8Q^ z^|w1`@A^Vk?OLd+Ri|{%;koaT@-1$jS9{f}b~pE{wZB?5P!J%jti^eP63M#}{>~O+ z#w5Z^&}TFc2Td6~F_RwzQRo@ff=)W$EfxGWo4e8A7)Z;X&Ru8r|1&q-rsx#Y}i zj5)^6F@4r1yN-}e_^Smf9%YA~YY4es-|0wZWZonHw@O z*;H@KSR}JbPJe|G_Kod@!KtuhUxN^<#zGUCbOe~jXYluT{@zzxvjvvx+t@U*ckq@; zvTw{5IUFcZw&l&JI$~9vfn-K3$}gctnj~gBtR&I2{u15?({he#v+`67U36E%UsW35 z5|_z~tioz@Z*Y$mpmI_T+lKC`T6WB}Fi&QPGrLmAb0evB5OCH25-nv4b(Lgc?G$+W z*~K29T?Vu6)05^*1_vQ2Q=L@h)QAGyM{tPEwqN+K

z-DQX&JqBM85k29WVhIURf&>DY_FQPxyICoZ; z9;~tBvT4~>^}9FB%52o$-!(4fExrF(h4xOhWK@Z^amgX+_mYxDkuo#tNu@q{H09Ew za78=mmO7aADick?F0bOZX(Y_7;;P9nwF4E$Ox`TNThU;0-%Pb4-$ZEhrozYMiX)>! z!=z`^SN3C*z24F64wI_DMs|`3C0xWdG0BcqEgv?CN!eIVGYQC!E?;HhhR3r8OsuGM zR-B0zi^u$KJjpx3 zVOaXb$mp&?>2|}G$4hCa4b?`Mlq?uzN&l7v>Nm;OQ)~1! zO#;0*VGV<+RHRu|FMU@LY*n~Cw!+y8v$C&ZnN_GwEBk{L($SyYYvr{`$R=Akc{{MT zT3H03+PsCS zc!3tR`kv%($&uAm=}+qPYD<{~b))$Mc`7B){FLGZX^jPNU;JR`&7L__GSCXm5J=V_TDz3Y`ndL!!Ndnz4ay*8)k3nHD7+uUMJA2yv}|}cu9GX z-CT@-_1o@q@_W`%yUA>I);7B_{92~W?kRN(v(oMkE0Ae!ca~ScxM;Vxsg@C7$8K+< z58LH+o}`D_MRngOyJzRq`>f2{&R+1gwAW5m_@#7(txz;eW7(3#|CNl{u9wV|IIjOs z`X80D{E@Sh zaT)DBU)tj`D7aN>?s7u-h*s@F5IrrKcG)BrmxMUaNuE>pIrm9lP~JJSWhxXK=MZ@i z*~59GVkdE(Gq9xcA6_dVsC?Y>9h6vp%ri$hqkN}lKU{GTORA_ZrxGt?77AP zR}OhDbN#`(;|Y1;Sq+G}fFV`{LLR=1rGfYu6UQ7tyiTrRW+R?wonxva#DxzTClGfi zuNeUd0dtD}7SYAiqf-&BO|Enm1h+k~tPVl$OfJ(wWOP$Y8xY%j>r0m)5CT4ph_Di# zE_v=7fqN^$jAJvB`xxlhZTNpW$j}&PrUudvWL;lEdS*2<;*oBJXXw9>4wQa+3(|}^M7KjKb3d2e@Rv8~ zmWBJjZ?`EO@gM2*DUIZak@%}h z4p40TB+^6VT)(cV5z+}iihKp}wjWxNTl~`RB3Kt}`=>B0&#-EcBVZb41nXffMSame z!TgGPY4U^l1a;qv&+JBBvp>hApia8RF}+cHJ!MQC)PaCR#!J+(uxpGq)R7onMhNOa zav*&U)t*J9??p8i?xMS+c$A}M&rnq6nKCRYj{BtaC(5(&YiT9Qv3+T&I%;*NHLV7v z-|b0LL8KpVDl+trv{uX{D?-yG)nwn$Ez$-OCsb$ZCh<+~dvPhVL(Z3*w=@~r&X)xDDQ(+wD>S#NyM5cPQzR7aNr)VXsC5)Ts zS$l6rBU%do!-z#c@!ZF-LyrWQ(tn|)VO;tES{5y%lhBjNYv>l}iLB_dJLuPi)Upip zGfGYA0{RYfcWDFq8256iDY~-pIqfhyx9vaLT699EO36`lc=zfObF^RY7V0iETo6I| zi#8XgQevYNq8#$+D6u%3v=DV%QcOZdRY++>UKD2PG~rg1+wAw^@hDS;Uy*GTa$$jy zm<%EYjHrJK!`PbC3yWgdBqeKHWoRaCGuEYlN!nx?OTU$5Y1dD0O45b5&|{L6J&|L837pA4FtI`;ITQmIg^@ZiI^uBR z$3&SVo@kqROPWT=P3)X%Ek2S+pM6s#OiWYQ;jI#>;2y%5tXv49qq0IERk{oIH*6Vw z8Ma=nx@-st4jmgraJ;9<2-DpMFFbayMj@`;kDA|Lx;g*+}V^tbEsr<~zwzE`&%ptyr zQj>YQYn%ehz3o~~K^hi%L*}}D?wVA1+jYLAGx7dYnInz|)UHmiSyL4NzQ^w;d zdQomh@9b@Sbw-u^?DtQFNxjM(TinTcA9ZiZ{rAJA1rvOa9QjqF^ll z7RUoJq&$J3QZ>pY2u6EP$%ZUw$H?!MgftHM=JF#nEV@AoH2}6dy8| z>q)*v&TC92dy-Kg4t}s#91&vYqnBohL1wtt)s*l+BOjA0xg7xd)$Fu3#VVnq>gm)66T7 zPDvuuMoGWKo-wPpu4E}=Y}Hfh7~}4GIQ2N=tn+Sa9;4l357mXy?4L$eVzi?cQywrn z(M^;F#)%||62iEMjiSI9H*oFb(~NuM+vH%zEyin-m~ooBloZZrX>=eCF{o`wBAgM= zPbTbTAi9VIh+*DSU7XHP?>ka-hdw{hUt~ysGjt!HM8EOqNgA%h^EdOt%6@@;n27ffvB)R#wZuKTXY$eYOaOZ zNeYImy)K)whBLSEH~AxH%)^A-&3Wu6BNuU=hBlKob4JnDq;y-sM+>Es*j{2jsIE|_=FK!fgR}GIp$uFonJ*ASjzp8dNBKJ;J_WY5Y z!78vfrIa?u0JTqP2CFHGVUrN3_ueKg*dg+&Mus+*^r-Aksj|^?V>Aib zXzkuaTHI*hMT!Gm{P&8U+?j@XTbS(Hph-`Ewn-|L(?HT^X z^hQe#qj+hfVZ((YRwLB<0xxTLe?Wl`Z@AmFtnggJ*&f@%RSo<;6t1G7VIZ$yq=7iZ zDllt^eN>+x*MNA@nOD=WYW#feg$CHik2!Da$7h{$H0t~3IoVG2;6x93(;kq2M>g1V z6{xph&o)SnWYS&(iy`K=-PF_|I<*}%{Y&`MwsQ@iaI=l!bd*ruhINMsUTrbHisIjG zJ3`JDpKse96;Pbpwj=RD@#?m?OxGfDTWUdF5wR_ubh}8S4MU&B?{ACZ*yC;6f*W!Q zPq%rt?kO~FTXWzBuDnhA*sFr~t@CH*3&L7ueXH`%w!R$j&WE+$7>dpdXzhBGnOo6X z^P(*0Y%6YjXZFk1?H>oTRa;$V^|Cg#>dj|ikL?D#bK>wne<2<{We5(PSoWFd{p>;Vv2fjUW&!!!ayx5V|b)bK|0Q>O3 zp^t|$=MQjZW-`|t$ea(zIDEh!sD0c?kb6L|KOqO|ed+`bc(LesHMAaopgVd|BtEix zm5~MBv|HP{yzoWW!iJTFyShGYzFHXC^=j*v!lhj=fTl<&DzZ$w+#hLSBf3_ojpN+jmvG3X7 z-n7iV{)cB%Tl$VZ7pGk8s~`KG{Jf9&(KhK@U&2gjl4YOA+-TB@OFw~kH{KS4C*N_q z4oLB-TYrJKvTi9u+PM=q1h5}DUBV~JNI7A`p4BIFmI^!V3E4M=O`F=XsX~^IW%dRk zeVcpMxR4g9n^hwu$KTJg6BefjVebeF^3G#pgxN$WQz}d=+m@LnOsw3Q@t+V~C(Fnb zqISEcKNBMNGt#|DZN z(&B;pVz!1>9AM2~@eGvNst17&l)pmhdPOdf{7le;|tzvR_CKmuvqem@%I_O{*A zgPp+6iGFGS#qJh8GhdAj7Y*7KVwZ|8x<1UjE;{DT$i#^b2OZ2b6?JZ}%D5rgA0L{L zEZURyH2s@scV2KhThvV0pRO*dEuBqk5LHzo(zHa(y0+9x5w+!G%8aOJzgtSADCcNy z@;On;>5gP|QOv~`Nl_xybyW;U2U4v4&tGyLK9hmg^jS8L%8{p4kQx?x5(?Xvq5s%a<5~J?@i()) z^epj|ZEU)kcohCS?WtJgHIl{@4+iO^S&MH+{7k(gz8QBfHCcQ;Ek5Oo_-gK}6sq_V zVN>#g_-tu&GE;o2LYnkT+*Rk3gcBcZX~T?&_jY{7z{R_cS|rwqYfeWejET!Hwj`_- zQ~E{mQR2Kib8#dw=HbfNUE+}EzA?weo5l*FFNs%A9zkCgt545GJ$dwH4ioj^Q74F} zQs01hWR~k|5M{)cz6S3_=DJrGpvCF%m$lI4ls}`JRUJ~QM>SW_QUXR7uXjrRE183@ zOg=05=w+FlAbA%^N%{?LOD#xhmQ2J2B(0T|$7qK+@4)6+I?tz9T^Y zkW>vz(Mu$h=NeHulFTv04qZvaBq35&;xRqAT~o4p&M9K@#Ydo%7ysP_lY#%B4RrGK zdk{ZQ3x4+)f~6qd_Cjkgf{ExwSs2R+V-rl`>j~YpzY=#(sJJ*L21(~UQxj&S-vUMw z>ZD)8r3q$IdF)X9N$G5AT0C6(Ip=F!pHx;HALlH6PaBLqCY4sKh&7VFuI0v*OJD4g zMUP3Jbc952ls-7ZK{KRxPF;_>F73a#WQSaOq2GIly7c%RQsi>!f#J^WM$%m)!x77* zRbxxSm8HZh#Xgap|@>rO zAF>rG|3wpJra3h9jLfhI42v>7T71+)nHKw8)MlAlt@Vz*vL(CfB0tMu`+h{aPW?XO zvYj;b{S+?Z_|%sRm&1prK3w}5_Im2ooeikbDe*8S^y$={k-CunsmpIf!F#7pO)lAn zo7y*x2@06v{1gWcPW_A1ANf%VbQ1Fetfnw6-$AZaqRn@AV0>!dHbZ(bv0qh{aOlhO z41*)+FnPF55?WavywN|ZNABZsFA631Kr(ks$lb&Gc4W)9#N3GdB;TAuh)kDn%2C_? zSngUx*zPXhSn?@izubY14F4-%UvoViEw|p~5Vl`#zOMuIO1|Ro8SGn$m zypRyN+O<=`A#&*U$8DaoKZXs1Y-ax(2@IH<9ecw=UY>pWVbDK+_Rh4GpUv#~pB=tA zv-=kQB+&l(OM=tCT4eFbe?Tu{$NzxOC3^4g07z%YiC^YQ7TcQ@@yiXi8!EP1U5^-2 zcsi|#z$@I`UqxsrT>LMEw=0~(mV}!tHpbY5?Nw|@9uLz~IA&L($O?P>Oz1O(T}fuB zy<%PYwU9c6b&Xf>8-@AKv%!{%75i+rp%q4l^Mlw5y_2T``3mjx3jxO!ORxDN_bH&; zEPvMg&j)Ax;^sd+llmIXPrR}AxjFy*Ly~v={Jk&zo*(9~|MWz>m_NU8H}=Ipd5Nw4 z*T(ezYok~GYj=Pd5mJu;wKib9uq8lYmZP{pVP2rXZ4-{>DDXR5!a{+e_(g?o0SeO_ zG7J>vXNVj4baw_n1EWSxunka{i`#&hP{V@eLA0eT2xM$j>j#4UKyzszIMuROCEy8A zm@NSapfCg^xR2)Cb^non3gZu=`L{Os*#U(K_k{z6Dc))X6y}`wbD%I!yvl&Wn1esG z@V(gsDAHebuw8`~Tm5U7ysiHRLE4MzL9Eblvi=ZIn96z}EbErk+W>_@*Np>(IbH{x z#b=@}3sk+-s#^s_(5hAp6vn?658f`HS{*Q6Z>u=~Rx{2uz)eclRD*LXEJbxbP?*)# z#z0|K@NNKwG2+Dog_+}i2MY6*3r5D?yIftMFy}d3pfE*M6F^}?tDJzsK$Uc$Fk=-L zfWqu#gYW;?$Ofl%W=hM!`gM-aeECmd>R&-R$}8(fA&n(w^^YKREnK35YVwQ7UvRnXFAecml->7M1F7fU-8TsfrLt>7{t)!r9e zHAqz;;;)-q%IGJ z_tC5KgyjV#)GolXBB-@jVCk_JYBOLdDc5SXVTswVYPw-@_~$h~uo&vA>KCv`*6nHv zY#VQ;+7RZ`^ow^A=C%jML%8+R4z8`Y0_tEVJH3 z%S7F*URi6YzD3<9&0l7Lb@wzstTC?Z)O_lAx(=s#)6J}Iljc$H(mGX5M&RYz$C@$W z)3wc-Zn5^YL7HYM+iQMl{K+QP9M*V`udYFAh^Z~rA2g1$_EcAB#Pj;A%{26yMtJAc zhuUBByw!_3KXFCsrrm$Jp=wvpYH;qVd0jT(_$+1KFseGE>U3AP%5cePP=%DFTo2BV z4k&$;ey&hb3X|!xw{wLiV0>@WRP9gWE8bzXL&p5Ts@f*w>hSBek;bH$zqOji z=_%`K28_eA!)kJjz46&KTE>o4a`hSG6)aYDsF9p^ls9QK*d*kY8XamE@l=gyoiDkK zMyT#D+~r0qdlxv3hVL({b5sp$ZY--RF_^onTlvla`&hXWsefY>R?(qvE}dt8S-w|h z#dck8E>ABHUaqG&$y75WgI>5<>qAi8Yb!PkR##;?v-DhDzU9br*SaXnJ1h6qxmupL zVb^I|9(UYSJ8F4!)1_LzWrw#-E#9&wFtyghk{aGx^T#qLW~An#Wm1ZAO_pV3wr!1` zWdJ^)y3f*`8dDu>xt^8Io3qsB)$n#&$eX%&Yb{>3pXHvn5O&_?dRQFkmT<0H)b#$x z*(uoE~;*XG)t z-$bt6Xm`qMrgo9tfk5Y)2X;;2`8D-+ihCbbiEJ+m#;Uw*TZL1V zXKc5NK2@%?T`c}kQNF%U@*jJ2Jw^I~Z3oV|E-NRk`z7DbYFPJHv5ygF3tYWU%@vGc zwPUX7(DB-8c-In1Z4ta~*~{8kc=?LQweIjD>yNd|;fW62HE-b|n;dG6!+pKZ)Re&8 z0)Ex_!R^C6Y8Jy+$1tk<;meaxRTscDvYu8O!j}}z@~*i2r7q$nxO`(R<0@R<@(|n> zm&Z*>-1RQk+Y2~-F2_47Io>WU-FvHUxUhPAsytot1p}4mT(%4ERa&{M5IwADaDF4c z$NuiTU-FoZa>huXl=Gd}%9L2LjbG({ti>B8ihBB6=lu%{wP>%Upch*_Z$h3mA3V($ z<s39(RVXGyG;{jKy4MNtDg9GwwqUB{F^)7D?j*`ckigo@yGY( zRDAGH63{E+{QZUX?A!h(q87Hj-;B7qyut6LWH0NZ-%cr?<>r?<^@PdxbCR1dj{0dT za>~y8<$x8zwJ?zJSbaK-3~5%=QIoI})v2g6+Pvy*sC_1qYCBY&Rduy8ieZ0{_Y76w z8p=C>iuZiQ%SA;7`0`v)JHq(f@2Kt3-?*nxp~(*1B$R(v0#_B~RY>QYL~Wupb3#xK z%>7kUC}Zy3Dmn^k{8XhADsNv@xhM2}=c>xpp)b1KDh`F-?G3M36)F&9vUi5^h2--2 zP_l?oo)8))E@NE_T_dStEf4u4t!1W#Jee9`oDMlASD}xDR4SrN7eYX^nJ0-+h9KU( z=x2~RuR9t8E9X_CziGMh@aWgZbG%6OUCUXXJz8Mz$o+%vg3oXV(QTd$+$MB$0F0ZA zZVk)ku0`*UzRMX$?@LzaG@)Cw5S%UOroxn}*XU{rxvCUh&a9|1Lg#UND!b9B#z&Q# z&@OH7DhALtolu28+O*r0eH*Ra>%#U%LxSM)b5SxOrra>ID5(l+vuc?DGz(!nf2V3e4Tym&xW3#C@Ff15)GmC)L5ixqFlBHR`#H zBtxU`+|(pxOC;9=Gh=s}tATj~=Ww24hCSUl9T*X^kCTsi7-q}ajFCjQRQ<+GB)zLT zg&EIUQ5B7ORp?u(z>HAhD%&x`%3@k13OibOqSwL#=R+3Ai{ZS?3WiiU*RdaA>YNsAiW-Vjf<+o$=fq>3Eh;%KSTj3w z&LZqmxIxw3%z1>QYFFl0B(^FeQ-S(WwE?Rf9aH%OtCQ4Qc^+$m{a%@bH7Z&E^-5jfx$3fd_`p@Q;AtH1DOTGVkpY6e#hTG=i%}3 z!Sj0WJ@=gV^PY3_#Lx0%@C?bfyzTI~js*lU++Su*(1UN5ugAmT3l*DiS7SaYU2yYb zx>ftJr(){VIk|N)`I;L!55YfglMY)_Ju`nkXxNAiq0UdUDJzDNVENhMeax|Z!oGL9Zs4QK1Oa2Ll<(9 z92t5Ee@FTvhUKm3{8oWTiShCpo!Y)0kbI zEYNDRb|-^XMNvoQ00b4C$!r5{QDzXBj(cFbbS@UH!Bi|RDw>U;;$&E#`uSAEeOLz#Zn5EVN#It{0EpE%(whR zOcr5YJ`58@-kw*AImigfGsM^n(g|dYzNiHM3;ju4jZZ=MN-p4r(3hm$I4^W<*D$sP zUDW*%tA~d7YjQ)-9!e;u9&N3fpFN6()O)j6qlPraStn4}v@bKwP%WT>qA(9sY!#vi zAd^$*NuWc9g){NfItvS)H;JySsBbaJ>G$9DN*Sa47pY z?&R>NEGBM?+B)+-Zm}i_^Bt?vcA=Hn`=Ej%fCAP-1@06AXctf{pauDDg_@g8xNr`(&y1~%Fg^`*BE3kv4644Iq5mK_)C-*J70FSWlT$)k*2T{LMEu)bpY1_>-G4rdL%cP3EpvcaKirqOh*+eafjLBs)_9}Q z#J$>DR1OiGMV)WMasagl3s_*h&3nKEXD8={Fx0SW!VgA}!AU|lWA}=~gi6MSwONFC z#!|;ag!S~lJBj!S`WN>kd^`PvmoXkk|Lk`TzmNVc6dbNi{{!dZt}*lwqqsE2VvIG; zg0TYci|u4ABGIrX7&GWMa|h_3`R{VQ>5oLSa%A*&v02V~dWFO(n@7h>y|Q8S_^!CD zNV<17HnW57)L)!AlWsg%g>k1%4Y#6mXwOF9q0ZB;Xf|djY316&^g$X5sQrt7{zAA` z4ypnPiRG(-daKIfVaLE#uhCcFHwg&KU*UBGscTAcJ%Zp(WjMCLeaAPPzrbnV1a7Hd zgXbBnQn1c%H}G+9us3@-|99g^7Lor(0%d;S%cUza1Nd!SE|_+HRrd+ZJbpoc6xxp;JD81P z@sAACGrIZfM{lK1@fT_g(j9mjZA@AYPY%>R_cS>B0Urp?B7^XAYHOfbI7E#CdXBv$ zdaXADnIyHHfMQJMQhMBL$@%NL>dCFhMU;b1%&rjOaP`@nL@46htUID4`syq{Q4~KYQz`Nn(ld{Ub~m2G zToSEqkz-6mi`vJ~c_N)Iebfu#hwjy=^}>h!TQktY_Q9j+mxPtW329?O!l)?COc*o% zG1XV-q1~5qO=twve*YOz&y2G;(+$*HdgdSm%l+LP3)$xUY}%%KDCeU1uj#xTf_Qv& zTFyT4@J7QNUGe?xlI$De|MtGgE)=&Nk!8DyFZw2BO^929|76vQ&&EV&9T%TVmSz4G zUqtI=Hi$3c4raQDn~BAkK5-4L9}^@N@b%HJ#HGRm=u=`sBLa0xoYX=`trZ8fU(TS5 z4_$wjJ}P#)JDI*!ysTd@4KIcUSEpWWd^fx|y1T{Ii zUoPbU@4j{EFlfCm-U9pXvo0thqipZiYMs=q!B!XJh^(sCwW~a{f?Jnv*p+44I&1rk z%tw+RdwnxGl8Gbknck8wCw^o8Nkf870PPLPS z^*u{TmFyq*fjlR%A6|-hD={2BhA?TF9Ir_ZXn8XAJK3y-2~G^CzX~!aIT@WGPm}$r z6J!Xp{#;3f7G_@l&soPA^SlGMn2cd|gj>(Tcy%0KAB55E*t5+OeWhd5o-63=j&+B- z(9RupCrGIG9jk*kqv#!0(M>3)jupvz8Hx^5)RBzj4kIix{a45QqQUf%j+r#O^d-{Y zytFjF^lP;=%}6@dFp^4<4!0~$`6eA`KbR6Iy?;F&*(z*7Ti0?2%Fi?j>hQ zQ(k>eYL@zsu1S0$bs5i0Tq9jNHINwB-VW4$@ogID`Oxhk&>p(g2fWzj=5xpZedh)r z_7znpyR;}2bxg*ztU}F~6|9TTxFO5fO3lcUMekXfu~`;z*eU&!EcAq4x=+6UROq%&z`nPWsW@i6c3qQH5%C^vutQY zZjqUse~PG)EofhW(3Q=&?vm`+HE}m0N!azWFDG%h>&`%B;^MBBS2q)Yg^&DHL9b(cB$;-FgO#Le_*^NqVlamjXrzXmY$FZr)J0m&VMSRkDJen0E_{+W zsi2i^Nz7DGxTOi>ioB{f2@#6yhNba6inQ}y@s5i4HfkJA5q3=$`&e=OjwW`#!lTa^ zzE|Nm;2MKetaz0W%~s4Fsf}vuAOA8L`MLkmR8Zu2e=*n@jsE8^={sKbgRCU`WeAYU zm=`Q)Ci2qr6sRS+|C#yh$H@uGZ)TCnYn31Etdia+hc+7|l`DJQjFS#29~?v^epTK- z22bQG<$=2sw<+&N4kRd)w-Q4VqLp3g*WTOq4FGed#qO3 zSjCNnD~0vF@Lr|h{0#VdCB5xHOp%g!4Htb^nSJL%)U-0W_j#0!GHhUWjou+zt&lHB*9EIz4l}LplaONGM=GQ?edG?rFwbbLEKx_%cFyFg{tSLuE#A^J&QaU zdtNn|@IH38>T$X^{E@0JrxSimb-&Os=Ar5?H8;jnb(1q3eNA<>YH##fReL=)imGZo z*A_XVYP$S8a;r*oZEHlDihBnUUZX1Rtq8lT!acng`bL%VYDUN(RoKX`5M$MmFYI6! zmD3a~=&uSq7l^>m;4VU}_;(gra?+;@z$kZo{0?%)86Q4COXAjzHR+s*bsXDdng@R~ zX0@gsUNyGNX(QZk%)pfjhmFlWU>(yirgzjMX7|{fQ!depu{jY#(V=6r5|X1nj_IU5 zkHV_|&4xS3jY4jz`Kes;Mv^qN|6 z^-IV%_1!zRA!h2%-hf~iwd84W&@px0PLjzl?5YD5ho{BN5q7`o>&$CGdzC6Dh(d?V`62tDlBedS;3Cb zHxtGs7eh}@EaX^+T%VX<$qq4}m{a#SICDbxoKaA>=J(~OAbri`)w;l)nh&>Cr-C)Z zy^8|UG*6!HKZ)1KhsgeUnyc?``{imRU#xwzHPt_=PCVC;{{D%p{c{57WaJ-^hmJk> zyBFwW(bQ86WZ@rUHoy{kGFIEDzvYA&iS6v4nBE( z;+(c;$mV#R_Qv~&V+?Kk=dPm}+U6g7d_-E|-@X{&AV4SC|JrD<^9AVS|9_4U{Zp7Q z+kXo4ufEV=NeGy)SUw8|O0vNsxB)24vEZP8dtrip{o4x@BmfHYJ!lC~7*^mlpfK@) z>%mjkeo74#=G3W8Kw$_0N|19|8USVtth|$;VxYp{BoZi$hyORAFa-Y?pfC^o!0+#z z<|khT6egKp0TkvEAH3Ih=Lw7z;L-E0mkF#6V$wvkm}- z`OO3-+IpWE1r+8g;|Wlh1O~Xj<^J?ipfI{LuznjHr)>lZBPa#Av(aiQaGY^s2{5B6 zNip%C!U&E-`ZJvbN1?g1Jp}I1T!S?NFgrJ$%?CLV3jx0i(zCVT;~~9G@q9N(Z~JZD zcSvu~B<~ueci5VTf%J}V=52=b0t2~EA-%}s+&pMbLKt@uG$+l2a|W7|y_2&Znv-wH z?uKR;o3Z1d*(_7`5@>e$a#j;GtL6`gVpg*b^C2{|b$}TT&FJW5C?H)~A!8E+lc&?c ztU|Gx_6dTNi)mXx6gNsKAc}ib@br$im;44%sDST)F^c~QMuLU#U&6v?)bV>@hv$d# z+h7Nb`}s_mhdG%KhwWW^kiP-8dt(((3v=6U!jr;w?D64|V9tlwyu+|{$IkE;z}B96 z$Grku9WlyHhFK)c;?9LFOVx7Dz?NkF;p~9T&wI@tgy~SG*%{Cz3u2oFGw<=T|ueGpJSfx&`kXeGaBzBw$9zd;{9ZzkVF|G*Ms-e3JKEBEqL`YYBf zeDpK7bKd?_4&2Onb`eLp8|VIs&)~e8 z+mlM>Q0CTVaX1chWAn<{eRF3~{$nTURWTp3XX~vmd&UyX5!a|$YvP$ohb#^LbdER1mmAv`V_W=H@0-?+_=Uy@`JSyd4O%nosaCe&eMObsbnYzUv;?$ekrABdlOii;= z*XEl&ecLAPVBD zyAZ@*zRC>x$*ZzHs)yqdtT!%R!i%%EUP0m=wq9bB%3EzcXTw_Vq_xi0X6{w1-@DDY zEUU4Df!rvo$45)K4py>&2b>R97sIDHr>*MatvJ3`f>dyvq*ZB_5BszgE-!?Az$%(j zz#6eS$gE`%tTvXNV;NbQ*2tK(mcN>wFdZz1THiBTEU$HZV>nnA%YM@vE$!rg=w=r8 z6yIqi3oJNGb=1N^^|{o;{K?o-YK3{u#Ho^N=DyRl6pSSdL@|HeO$g%2)}4X|cv~H) zbNqNq9g>Z(JROIK753aW4##a)bFVt=Ua#WP9JXyu;`%!{yNz(|9Bd9caDF;0IU2>e zTzQojqp%E_Df;WB)R1Eqj~2B5xb(h5dC(I4jq_iJ8wbv1gaj znN9ZintJ9I`^2UWhRoit^#Q}f-nHX7U1q;j_MX1O?y-E7Cb1(a-q6hLb|^r`uZ$T`oo#a_d`bdr!>0ulOFP=%zr4^b4tJ%nIZ3H`^>a!zFAs zGwa|+Y&KZ^n7h;Y*ZOkq0_V}K)|>(7*KQ4*GtL7CzH)M$z zp5g(=^yw1v87J^w;hafI_mk_e#E_HpVlg0V%dSM}+^WL?|Y!l~>YxQb0r^c1K z-iL#7#cX-W+3gzR7Q!)d4Lop>{l?YDN6S9r>K?G09q+m=Je6(kx<0O))#GZD(!oNz znrF&cOI?i#gUqY0dX$;WV3(gvd&W1HPi1ZllFN&l0EVf{-KG?Jt;?lWBHhMC*ilBS zb19NF(w4gf$j_G2Tx=9g)ZaTbU@jc7Q>waLa&2e6dbDKmPM-;f;$1uKr?bhccU}fx zS9^RQ8-h5}1M8p#oRs~3us%-UeltTdXW#w>%S$+`_RmZQT_6p~vejY<87L zuiH{~vd4`BD7K4-)aM2p<{=3%U|siU40mUtJgVYSSgSm^DGcTV4{ByD6X{VvXkjk# zK#^ZCE_uW;XD~cHg3FfEUwC-eY@sK4xHkFFrabIgV`#Y^CLP(OQ|`ZH#ia;$g`7nl zc5hVBsh;lHNy1zJb-^hdsrF5TcI+jFsUksu!_Bjip(D8vo^VxU2 zrLYFJ*c)L0XEVJ6m&w>^-rlRwY%lNK>sV|H?+shrSRcGs>{75gycZqV$|~}n>r=uy z=sok~5KGtVZ@4M*Kd&Eg2bl3*pHmW;i@e@v;u&pTF9|e8h*uA}lm6T58sjaU4N#JJO0&6 zhgePiH0$TALVw)4m8_Hg$(vuZto=iF6)->h`|khBl=^%79A)DDeNLWZ?(p{t`^*^k zKM-fh5c%&;IlwsNzdbXQ{>gt6A%)KNw;t>6Xiq~8aTw)CyvtER=J z$$k%8*Hg#+ZgqH26aCK10!m)`70IJY4)_HrqKjMnHY*XuhJFSrG$qkjt!^S;^S!Cj zkUsg=PVXVE@CBB{+8b^UW;a{Ien4|sE5kBihnc@Zzw3uGUxyBv4l-|r_F8XZ)`b4& zP|3^)ZQPv2+#Sl_<-nX9O5fkico<6cF=SMRGEbtx7Z(I!w-_d&+}Pjr+o6nC+)LEgkI@Xlr zg?yE{6l+3ynCVXlnZ1O=VEm0;H+KQ!L+nBm5#xR=%qor10RQM{Hrv_`{SLv_g1KrUh*c{0?Dr=>vEN zIkq$oUeBOYr{E+(9Tg9c6nNj{a#g>UVcQ(ORFEn7*^z;)#Ilz^Ba z#RhUmj8wUUyeNjP@*u%uQq);QX^gibI8*7)h;Od3v_FUs`*mn{5u@H%8XNK1 zf07o2m^adq#*pdev^klrbM5-7h$9@Bngwpl`Du}lY3OQ#68JP z>X0H@GD%Zkcq2JZ`=elga_HZ`^v#*05JX>_DFf|{OkcS!I{<7M%*N=&fD zIT{Lc(5{!ZAG6+Bk7kH5ahWQ8hSB$^E3L!K^|miXVhsJymu|%@3p1wv!q~(nP&+YB z$aB;*u-C;08`#ZBd|$9n-|;Z@_kIuM)=t6C`JD&C@5eBL%l!- z=NwZL$q}%p)QzN13!SO+Nsm|Nl#GyMw);!kNllK!B?J=91y*v1gmZsXVnoXILKnXz z<@?Vqt|yg*5{nZ^Jox)!J5nvui84Yuk4d1ElREHxiVLZk^nm<`#G`A;K_t9j3F$d0 zO5{ZHC+!y>B0eD5O2UXcNpq#>qI%+|u96}{;^S^nVJcDD-&}BySUz~Az=W7H{2)J= z7^sHjRS>spju7qbSjlFrzFzv_?@O~qW6TZB~Q|y!b{1Q z=qiMYoI?MISwXg@f5#sq-J+{WWKuZ&IsF<@O~1<@CdSfRL^?$(I$ON7=p-E@*;IIo z9w7krMOo$!J487l!0NLps|6v;uaUI^&o!CkyMk?-ipg|= z#f}lOpTJ<>2-!@a=UGD<63q77N)ie5Lb^y10zR7mm&w)97Pw@p`loxud-Ep!Oi_pTAJ{)`E2*`CBa- znnjMUX@(w>Iz(M_I!HyLie);a08yz8nq(=;*?5mQB8u5@hd>M%@%4>5MRwvd+v^HHi3NL=g%`!_BkhH$Vuo*Yp{hDF3BIab#zHrKI=7Wd3oCCb@;G%bP2? zhaD%JmE0z7B^;7;(hBfHl1seX_(Vywa0c$HMAWzumnPw~oWc%E2<>^;gObGSwYd_B z-`%dFUgv2`7-L2wX@~%cy(re%f+eb%wsLFKzWns>H>6P|gcD)@OXo{V21;I_HvQtBP`SEoCc8ALU$@E#X<@?3L+* zgBay9-G=IH&#vk7Jz2NA#@lCRId{Fk?v%;tdUE#^=5tq9UpmIEt7(9PF6?5y>O^&Q zWsXc{Om&5gAIxy?a+_+-@VX9iZ22pDYJhZq>`4V}$b&Dyi^CtxgY@xB?mdRpVL!{e z3~pji%gfDwW8reD!zt`aId+RQ_n{oIJ3f~oPdvoP^_0gSN9Drg@St5e7vzyqcXNW| zVM*I_AbDU0IlD^kmphofRerR{I;&THxD=liF5ky}m^mijQEi=>BzJ5$i5ZjIo~K~K z#47_{clP+MyBTu-X4SfPE2?u}d3h7~IKAI16%i$~D=x614D6X#Dm!qd>wcnF1 zQ`ByLmyK4i-L7YER4@+y%6g}uAA6t0SCj@`&2m>zqP#N46vV`PnN&r7hDD~00-KwK zxvI!4?8Y2Zq?g*FhZV`(d^9**v1$`<0(t6oRk*b)dHxoIklS6+Kx>Wl|cE*2F zIeuZq+o_gKL2*BZ$3Q+i?lZWHvK&8s1wAkSR052$=;I?`d?O!vp(W|B#@IUI^toe8 zO_S4ZjOnf6r{#|6I9a7_QU7qorH-j54w$5J)e}c|rfyejz?Y*3)#DLQQV{Ad36UwY z)F0D&k>%C+#+gV70n@%uc`E}jIL?2M1!rh)Xieba>&rEZ*fp%b4nV`7_e22wJy%4P>LdBWUr z4RXzdu}cf$`NYEgafqS`L!Vm2+6lvev&lCm3?hP({U#Q~e@S{jF+c585_&=}`(cvK z#LR+qiPQ;LNn@g+<`>5#fufnJq{RQ$e670|pQ!nKZcbdkX7uu@I0wz})r!~*&C}aT zxI}ZmcV5gJ&Go0dW9Diu4i!bO)ik`n9p#}Z`?4xBQA7Gs6`|H3|NhA^{<9AB`Nba) zMf#54XF$$=@oyNAKKQRikRc*$`kRhf(x+)hqtPVh^s-geN#4^-Ht8qLnKs;cGqH18 z-=iiGK0VL-YvRo5xd96k%BJUr%M6cr8$vA~^W{#Vcc;gVjDICLbt_Nz7cJFh`?BKw(Ovt%1Tkj{>&R zX%Yo`b?aCp5hzSn#7m$sbrH*f!iBZMHsyc#r6nBO&S5M+rFfqTn#Km^Rvaj_^7C`_OTXxUz}a2O~|ov;ij%q5{O zP#AeN(1ghA)nKGcxK_OfTv1^anCoYgs$#%<^jIaRm?=J9355LByQ z0mhN$c@=Ac!aOU#4HQOR?g3V7VcPVqNWw zihRhX*|1_Iw5s(-`DMtm<9oRaWG3q_Yle*E=Rp(;6&ZpI$WR%?zXutrZ1`4?fqEk^ z4KmPd;Z{P1+A#K0$P8QyX#O_nh$wtsB&=NcXYPWzYT6e z`Kjb8>g zj?%gFpJAC+r|gDdS+b7Dy*YxHIvW%k!G0YJp#4G!xd`eAw6N zFm|yHA6yIhvQ?0dXp`wQ%s?nLnVPFFtTcJDI8I13xwqo3Fwx|S%^Be#lQSE9g=QwT zTYIa=Ojx^jR^Kp5K8UI2nRp&;t&TC-5HMctV6r5_tZK~oXZ*gZT4PmeNR^-Q{j8)a z9piI(nU&{_b14;-zQ%i)?G=+tewTGsFqWLJd0eq(NkP*{`IRL>;94ABvi83jWserW zxS?O>yEyiqo}ka@kK(6buMtnF<)1e)Q)&3div(lKdFVy@6Q{Vf3!hHcu>BVU?-eDk zii05GlyyDyRhVMcpyw()VTCuE5V}~QR-_3ntm17VtAAUCY*<#Uu=3wpRb6d$Y`0-G z#>(~J(P~dCo1?_)_ryf%MrrA>!=`##&!BokI=|r z{_LOCllCJCc*Sq9ZB@SE z8`-9*hIqNQo5tpGU#`_m9O3G%eLBryb=d-M7lv#}2T`op{0l0nu5ccnsj9|1cPzqJ z$2+%}fp020*R8%)ZR5;cUsCnUIe)83)jj7dx2mcdXT*UIRcPn%qxMz%osS2^S1oXM z59d@qaNZVowX)E8L&}TF&CY8w$18@NE%J0KN}P=->na?bb(zP?A39BxMVBW#4b@=F zXE{A+;*^y;b+k5?**evANCmAbDlar5J&Tn-xQrza-IX+U}=aoA$RXsdi$3XQ@ zZnUHIge!;Xs5ebw`8q!M`?vbqE-95-DI^WH=%EVRYz?sUAErp9PZ!>EL}FpPV-N@K=LNO#m+uO15dECTzQ^1XJ>?}m7B8DPW_gn*zsdxEk|d^ zqiGCNu@k%lt275PA*ib7z%pn-RmOf}*xjlikAB05DzGJGIkIZ4NBgSJRk|K$*Ns*x zJ<7HaE1NwE+!j>gJTM26Dvx_4`*c)Vcti)xsCe%Y7{0Bd-s5OoR7J4I{*=Osc^dMn+f&hksfQwkIMBt%osn*#2yRFmXz)B(5=}d7+&nNdW~;!teoxja@U=T zXI>8vtgC4D>h{U2KzqqfDk`>nb%f6?|KTNx+g^U&OPms19_l5`Oe@#(;t}%8q+TR) zYgxEg7UP-VuUCp-Qo!?ys#zph=jGS5j^E|w(dxnX@^bD7;yv;*lO^$vd5+65-20xl z6&UVr&njge=bUGTio`MUJgC0HPV+RI&|x(knVvq*sPY7J+RE^N#Sm2KeX;4%xfxg!?ciIMRZsw%)MfB=r#Q{6=tEe zrqc4ip=|5rZ4QkKGgGMXxZ6Nudv3l z_|QYKsxtG?eJT2a-cZ*}8$nj+X2MQ^O{gsy!S4%Q&M4<2L-hog`7=VkiWI!EkhkKG zJlhau>m2TdkOv)>+$|v;vdtVx2ur??vpOU}agfak*{k$r{R%Ntg|Nbcr`1emXYhb# zkf90gn6{;x2ZKARTo?!DBjxP46QITf6?+4kE;Q5&| z_~r0i!ZQ8=cnaBvHx7?tV0m=-Q9&8c67D3r$Zdfy6W`;y!skogbMC-l9T4YW%(%>i zEsJ>|U(I%oX;3(@gfRr=c9w2TplTm8B4(>PlhGMtqPb3=jMh#Y(hkG`wJ&Q(se_=h z%71%d%JPw8P+?gBa?{MOWqXhYOEhJxkdS3z8HD&`A0>E-cG01#SPnuPA8F1a%YwO6Fl`g}?`suEP>I zV_X*>5tw2eEO`8Hn5A}C_}v)DNz3P<)h-|S(dd^R<@^okLGKm3ALu9k4BmD0^U&`+ zBKlqIVcuc121(=1LjS^aa$C{U_-EV@^f-ATR~P+;v6mx6_X)x|_UH?uY<4S}C8o2T z&?w1i)&+Es^a^Ve`cT&+W+U3J`z_NLt=IpFfkllB{-O_}ZmH(d*P&|FZZtq58dB*M zRG9V|^%TknR8WZXR)ac_3IeD{IX=Ux?3GB@ZZA~;OB;3;D+IiV)eP!cnhRI=QVyErkGQK z--f@$*^l2q`o#W(w`Q2Li|_`5UF-$8uc9Cp3-?rkI?u?& zg>-k(-{W@m-=iPFtsHzttH=Hs9xMHU9a5W@x?`_u;;6aUa&2dcD>esIP*5o)pn?KN zi3Y77`8%|Lx0t*i_K>S2-CVek+d*nt8Oi06__j{mSQ5eUA=jA{vvZOQk$m0na_*D7 zy<#{ll9%5nP9W(-XdK6q6aw#Nza_;XP1*G%6y_*9ghap-*fU7!q|2-(5}dAP?IN8J z%wygmZ5P=y50T8odl=osKa!ISH{vTPl75NU)m1>ZCyKkvX#yg(zp?Ze5iuw&^&@%@ z-=j7VH;n!*nIJCE94c`r`lzicHY}2Z3W_kM3RF;dgGwuiYsvJ2^f?2JDj0`T%$RSW z%YoA;R+w=1&{b=FImYz+j&^Jny>(|2`z&4L?$5^1%e-LhU37sTn>~|W9cse5M;F7f ztWx?##6y-B{W`{gHHY4fcW0ib|3}JUdeR%|=NPZ(T>e8w6dfm=q`#v_iS_AG^!<|6 zG$q|u>P9UiarwxwJcjypqo}@o;mA8;vqLR9s9;(AZM$(hh2I zT43f6&(%rGi;F zEn^e^C&`iC$5+$i=yCiPd_L_nUoPyTrSK(<%F<7KUdy-ANPf0-A@w0Ytjmsii0{>X zphU{w+88UV}E2l*_||+Jkwupqhm>Q43bR ztn;I(Htjpf$Q0Rm zu41@|Y0VZBE-dL^Pbn0p4IU)F z5c&^Ck?n;$N6Sbkq51eb;w2%Zbt(E-{Sc^q-x=WD%+@m?+sq7Z{sF-l-Oc9E8pZ;# zR(BKqrTCgj5B-w({ObL5g1BmfhVCw=Z0FJEiqU&}Xivl`N6yg%;zZvdny)xE_zP{m zI6B6^^sX45e7Te$Mxde6&0;j}5Oq+DAd;!+;s~0YY9J2a&nT%Adkc4$IEnW*#ur}@ zuWubu8 z$hh%>b@E81+y*edf< zh2+@=^HR3t?zRu5zLK`RyGmC|T8?a@zLuOh@sV03X$&r<`bthmPg7@0gvtITS0uHl z>XIZ$6ZS*N0!b}#eeoFykA^BfASvawQ=Ur-tEVVQlAOlXczMCG7RJ=~0*4lGstVn| z6XeY4n>raly}LU>?u2G=Wev2j)VM>dvyPhCv1jo?>Yfg3Dw>gy* zNGJBxmmHCPI()8VfmD40T`ZTr4_;l2m#U&GiZ@DMCjFwklqykf6pHj2mO`EY^P~*Qwr$OzFl4rSbSVC^Rfo+frZUUp zQ{>086+vy}5}9eVGkK?MaZ)wuoy;KP9f=~Fhjk#?$aITRh_bGqrMHQJU0-=dMW4FH zs>6!1yHpL#BHgaR^EV2My87CF7X0bDb=|fgzU%VcWBEN@HGN6>_FcsTrFrLq+s>}i-uKq|JM-;V#R*w~hhk5#O> z)`II*EWa}hKG{0IZ#K5De|lhj?vws^uR?My`+G-tIq?3rFCVho`^%<$vrqT?11XMu z3G(FR?Jo&HiaTB|1k&37;xII$2>W~!R9WyvIXZiDLAmmp*_whVWt*K&!3w3=`A2@A zvV7N{{1PSa;Hmt5O3pFgya^>IaATfG$&8fe9afemdJ{e?i_^~%Xv(5oeS(8BuP_LI zSD8z_i4Rs{xR$svWoi{3hf>DZcVM;3h;!pujPg{QbM6=A(Q8S$eoBu!H94)yZN2w% zW-INUYO{|kO>VlxK^KYqEIxq47BWtV9%SPrE4$h&sXbF+8g=`Iqyvj)a0 zehX$J1$*B3Le_cM*YkDV^X92~mst=Vs&23CA<$Kw&a(+_s+L_l@l&ei18w*gRnyTs z_z+dosTTY!l{oSs?!2l#;R)`zsy2N)R->xUsm0P%csHhAHPvn!~I}S@z+nEL7#7Yl7n{Jrznsc-u)4*VF`~uv_OmTW@9-z z^|%=IUsFHaYW4UUBKD1X)M*KpuO4zu#U57=9?-|mRu3L^%)P80IAxz3q<$LFoAXos zBq1=TQvE3HX3lnXUygJ3BlZ0PYIdUfE)|ybM}32nm_<@wt9+WJukNT{l1WuxJa;Tp zSAF(!H6~46cl9M2d?WLYDcVY1+ItX{q|SSqn{h^+F?20mrH*<3Cw+<9_e)@!zk1h? z-qaszkYCTEdX> z&dP~dj!Uy2YyP;@WYaX-{lVFLG*do;tdE+hfYVtF&5wu^S@xRm@$WNlY9`aXGlMk~ z*|#uXHJ=L1F+`2Jq#UzY^NuqQU8Yf0V$r6Wmvz@s9L?a_KPWxT!^=Jy>6&|2S?Lcn zUAOzwmuW8d>ZV0#nx1Y;6>2Jn@F|Zp^!Kt9T}|E>E97ns@<%1&p62A=KltT;=72tr z{T=~r(C=bklu^I_N#Bp3-@&=T4%07mmS(=3HZ$tUq)g9PMaw)m4Q>3JsiXb5vkmi~ z_PYlclc=5a9>pxuP6o_DOSF^WH_(T*-{PZC&$M4tdr(NNCVL+W(tgUX$lz(mO3X8? zwC~xRbg@=dX_&r7`?AhAtzN4<%S~IZeSArsO40US-H`HDd;2yr#aVl$rw)nLwmf-; z=+vGbT9y1+D|jE3yh2O;d?(3UOZaghu}zEmJCF+;1oRn52WZ3pwb@|j3y?l@(9XkT z{8Jb-*v)QWie>|a`HT(#3gdvb1PbGi0`lS>f+_|I(}@D=?EfdD3n)xw2B>L>L!`e2 z3e%a+{iiVLU^L7tP6H!*v2|J`P#8v<0a){|Oceoz2}s=w6o!}b2q?_^lxUzZtC2%M zVZxBUKw*jzoj_sQ5I|vGLdk(ZVb&&{1`3m#*aH-1GSMC=Ou?BoU>6Mij3opSo|6Ryxh#IkEF=UH}w^+yv%}J8m|COyXX36A|TjfHjY5Bgy)S|2$pua(E@^H3mZU; z`7sUKKoqg{S3ned{~yJ=|3_h1Cje0x)j5GE-kyF4q8K<0cC5+h|BoWQ7W7$RRs;I1 zoGAjM&#+Wv1){hi%m-0itnLO;ysrXLgD4KbHqKbuya{?g>vr=Jh`oT*^aCm~ku*Jk z2o^`0Dxj>j-gw6>6|&Ag)KCOj=9@QILFUDd^%o#B){1&B$h3S(-3YX(c4i$7S_rNM zn76flJAD&c*zxW3L1^LsI6CXNroK0h-^~uBZ3&CA02L)fDGM-Qg317uQYl3ys0OE3q%1ERFqjT3-+~`JuHU7@A&)YdGX?<<9zP!J@+}!`*WVt)j?B3W<5gM ze8^OjK+T6twGotl$W&)e@qlLOUC8y&EQ1T_31ni7Zgz#tfC|?c^-vo3+_Y7&7S71z z)6+GaN0TR6!#S5H_1pMxsFN5D=A7(Fgw=FT@TAlYb2tkpsy1h_zfbVq*2%s;Vbbnz z>}vS0{p;9~@UMpu>;-WB(HxcrK6s*>RRjm8RP zWys)x)HjR}xF`2FT?bz)gVRsJSGJkb#^6h?Or@QMTiu#K9fnWQyrv$6jcIk1PS^+C zRf;3*sa{XM0J}9}Lh6BC9oyFQ5e}YU-h_D-&;)M8To>3h4s$kTnuc?J_A#q%oYdI| z7qxQ2X75-|=D5xVU4l7NW-s@+#(q3|%4S!#*!s%0batUNb2pD2W?dEej=jkG%%RyV zopsF7O{^yC9Va4Ld#pEPC$eDH3-dFWVyn?o0yEO;ewCatW+iTDXVh6?sC|qzRtLB* z={;5}q+jVsOJf_1_TEweRGhT*xiyLU&7xQHlbU96So@9gpZPo8drG)@)VtZ_F0&gW zVI-qj#aLsL!m?m|oXY{bU?8r;k^tx(C)WPk)Xy9*`zw~coaOe)MM%zcd%Sp3F|LKeH`}?QMT#cMf}oeOTmWR=@qG*bl5Gd&i^JtO)ygC)`*w z?9Hu=6%L?E7 z>?tlb+vc&JyO`__V~L%OkvP^_=eMz!Seu-+Nw1hj=iU<&nIh*K*>joE&h7b5%$d%- z(!GpUXM9yAW508LLlOOl^D$}@y~;V9E2TR+`$@0SE;&1?AJBrGVO=k%{Z4H?I%>32 zisl(*&}p%DkP_keQul;>+mZTi3VEYr?8sJ9hGXDZWs|2<%J?{^d3`KUv18o>=p4Ih zZPuh1cKX__7Q5N|*ZMB3V0*1yx2%OdXRX7kT-L|6b3A6UF0GyATf?edJ7Md8tYhxq zcQ0XiyT6G%%$n-{I2NS#?%hdpX0iLF^!rS2_tva8jQ8%syirEIJGpczV~cxb)pq)8 z_q+xq{k(fBHH*H;J&KE?Dc!e8>9k;XcXbQ(nY(pY7d64{dCzsq2REwb3MI`gR@+T} z=VqhpA;-AB)sK>TU8N%%NmE_x#|jz;+``7k**`Yjg&?+U(?MuHd%DjGxF_qa_fKl)l>M(^d+U9?*7 z+1w=BYVV2C9O`wiPwFZv!s~t)nKI->?_p9Bz0x%_vfj&E%Ol5mP0)!*1D?0^I?^`J z`jKVL7*Et#Mq?^iu#U5>ww{F`mVT=VG>erTI0(DKLImcTZDefi zCu7DLlS7Ust)zE_#HR12XNE*%CD50Igyf-U zk3)P*ShRu=x5_@6Lx^Mj2kO-j8|qAIOo%DhfifBblWwMz2Y*rTqbv*_=t?59g84ls z$u7ZpnhcUKI6|94S{}SoSJ>PT4A(20r|f)e_}jE&r)+E&5wY{#_&D=;qzwcyBmYqt zrc=Ze*eT}3eVa`#GCu9IoyTH4+-JH(!I12muwtB1i1@MYCIf+ZvniS3hx~RuB=0p(; zkBJ>o*<%j%V^KvQqogGfAc#&#+7C^k=OjuY1O0Hq^=T*RAqk{85p>rC+~Od*SpwSS zBTb)heC;spV#0o(7FtEZ?jV0!RKku>DQ#&&V1xzrO~RI#gH&F^mc&MCLV{1)9qRmq z4X58xdJ^BjejV4LGq68Q5l=N z<5gW|&7tu%J+@8v;*&MYo3_UXY1cKj$J^<)G&;xq)}x4ZaZe1F8=l5p8Z)h59|v+? znmSzpvP((&S!e>SG3^C(l7>nhpLCFRICa3fix!l6%RYs+ELG{Um-;7_zBY||FBR(( zLZzk_2EM1Jr=mmSs2ftV_w`c7Qc*EBl+M&Mi3yZ~)Vwq*#WNLs`WpFNYF6$bxj8kp zWHvb@^>F1@(vMU`{V@_g)sIq6a!Fmrp*MG=nn>E3cc+Z2`kS7oeD3_*bU5X1_ju!f zDLl=T#seuu+PTE;lvv$zqHBt`{$K+>Wxj!4Ka~92_?-}${1LcWE@pvjnkvo$H5_Vn zmM1iydgAm}cn&rG^mHp1>Q0o=E|=;xRQH4AE9j5j@#Grxi$F*6VRU~;8+ir#{yulo2>M<$p45hZl<<+1 zhaODxAO)g7oK7Z9K>y6GZ*D<9gF;k#3dD*MBQ%Q~QL+klm%O%^VD^PPzxa&JcG76^Q73!Sz2b1U zDr`H=&da+Z$MUrpv;*bNRX~l~WgU$Df=SK%O6N>E;IL*<;&Z(o#ON!T@c5l)Z z`{l+p;fg(qiA_GmD{z{|*TuGkUyZrNGbr|rrkF3Bjl_D)eaSB30*qXh*wBcn?K}rM zT%PN$ttVn)`e^mDFunr{LMFy`s0aU`Xngn&-lb^Ju(d9&=#sIic3V*sa8PioJ^=?s zQ`Hp^Pgm`LW|Dj=i(pt19QSh8M(*fI? zr{U6re>6S79Y+*2HQ7B_={`!>g;n&O!*^nd z12y<%*mFbdy7O4%@UvPic9X%Ob`f@w@odeEim$+XKp^e{-UAGAIf#)BDrj1>N&O#~ zcT;=4>&(SXjrFDr3YxO&!E&f+7vY0za+3|=p=WR72f|hVzQ%S!dvHY~hM+_&X$&T` zBH4{rgo_F0#HWO-smVkV;T}ptL=pzjABfWky~WEKgoKN@0}a6hE&)^joPZ}w>k|pN z?AwG-ggEgVLKekW$-$9~dgEU4U;NR@bEC zNrp+)Dm>bFu&M_i1-u8DH1J$E9i~x0TuF0=CNPAMp4d8HE&!;37@BIw4ekn z-${H!S-XZ$Y@yhAt|8`8O#DNLyC{=`7ZB%BVBzfz&&hv~K@BwW-*{<5B4tvlMS~;7 z6m_8f6=g2EsUAz2h3T(fP5z5BCEO-|By1+clZVKe_%GyMb`2gyZWT-FzLOi3nz~f- z`HqjZpU5Y>O=^#k5BDvvc}fl*@UQVDyA8!uH<0HJ*HwKa{r;k>@*q7oZmdiui9sDA zg%8%XjR-z)9X49=`XCswnL7q8Z}`UPpRlB%lhZgy)Ij81Shl7inR90Ke+?Tr@f*t; z;G8hOEA@Sx;GK;6dd}AHJ@p4Teo=4g=W#a0hY$uhn^S}YJZD>`k+6re2knO+<7~s^ z;~O{|u^sre9B2Hux_ca3@|wChjwO44?ML>w_)P6__8TRs=0A3CM|({S`|`~f)mpab zp0Rodn=ml9O2p0?@~E21J~W(Gd5|6SJN{v_mMoC4XZbobT6Gk7?P+~&mwRn%ni>8DUw=0cY>z`yVH>{QBo52 z5`SG%7*&YRm*mD7@Sc*hDWP>=CFo2-9amD6^SW-21dVa1{VYkt9;+>r9Ky3|mr5c@ zZ)+|}LfO_eAre3F=IUO_O6AdNf645QvZ~9HDK|w`>%>3r-L7Pchx*@FP7_}rGQk}Y z^M`}53UTEZJl0&CIyzGEPCOrY50<%|B{XU6XcL*Py##dq3CsbpAOgtRC$&#;vFpbbd=FR3ke-pL$U}t@B;Z zvMPS(e?>`Eft`;ll$ArBxAE}Gtj;T>(8`&eYF0k3sZ%PF;T$?SiVs+RXLH8_?6S_P zn?V(X&is1`6|m0a{*v-ToneF03vHcVZ{J-o>0JC}Q(00cY?M~!-Ju55eogOgF!JfW zivjN@-2v63+84KPKr?IZ-)ex>SHJ8jHI1oe_Z+e9sy@+!Sm9c|rYC59W0k(gWApAR zd5_!n?5eDu)q4+BZRl}1xT@07<8VY-$>>>(@8 zy4n5v%L2M_gGHsp?yR?$OWt?Kez7ds+P!5oyJU4Ys7KW%J_5D3THi+jfOq#jyaVFY z2cyums*m>{!d6$_(THb!uPoD)*uW|gnjDwem6n>cb=PsXG%>ymT(#!lwpX})n#etU zIBN~!03X|{*?YtXo2v;uei7@U*>TFELZb;hceEl?v$?Rf!dl~1F}qxo76l<#`IlZeG)K;y1RZ*wS-}JfSfHr4aaK!@c**)9JA8NA?EGsY7q7HYI zZ_s8O54`YFo0h@2P@+A4ZsG+OZ9-vq*=_Bi^7gVsZB*U-(%;&B%~_=t+EAvfbiOu7 z_^yPc-K1DmvPiq`azb&1*7*hzGorP*dmXb|Yx?+W5p&@8phMyJfwylD6>b^m`>ZUe z9cUe0R&Zn>7m(tl*B~>m-1HjoZrsDyQ$U>Z3e;RH23~qY_T~F^KPRlY@KvXpH-6!= zPVCrvAy3D0|9-(kSHEdxnNe50m0PCLRqbvoOVL&CXOu0_;ST$jcI(QI^_HH{m1ek> zTI-6=5=#`i{DO}q`*i2Z4-^}8r)xWk3v?OHON-5QDa=EdCSAOch_Tg0x4tZD)FCe0 z6iwCbys@wFn9lcZVZn8s+vC;(Yu(ZpBj*q4EZ(ll=MMe(jL91vdi8TOuYE`Xc1EB1 z2<{^6u8$lr@~0pDL7e=d3Xsa)_qC8!ncus3IJGoiKRkC`=@z}#A+>aZ{=WOfk~aNK zpTi|*^<7(`k`4Nf-44ZieaHUA#kKm&hprcU>MtGJiy6|loovUP*DKDhz}V|$1?P({ z>qQrS7Dej0wTBDef;C%j;YmG(IkVuGo+#W_a86&{ia-BDUv}xy`B;7K4YT}C{i(ZK z^5^OkAD_xg&>wiA$Q9{>-~2>>(|djnM{m|I`FRUap^1R827K28#yao&4KQ-@cQD_U zUHjGsNM+vF9>}8j(+C%S81r#t>g@Fxy5akBb4;S)v)fdRt>KM#bw;HZ7TJyaO9fEIpZ3cB~P@bDXdZ{G0$-usT6aCB3)H{X_H&i}$&7m7W%i?qI z4X8J5=T;h$J})|Z%7FOUaCWM}XZ&yJ;GZ{O%JCA zZTYW^&yFJV&l;bdxRGyZd~(J!PhjjnUzxYV_~3$7?tjL6HD$S{jJKPnqpP+H!^!kXJtm>qeo|9M(hi_Guw>k-^6B@7*U_D zXI(QM{kc8M!x%mO6hryPTrmG9FBp@5KNki5i-imSQJC}Ko`aR02V}%_!Fk~LwEuD* zxb#-9&Iet>JR|Z80fmXm_W%^;a^9Qq@dzj{3s4wZ-cmqeGIGI=<@Bq$v4FxvqCWu& zQ-KD*DRV>vZ&@`S4L+xFc@7DznGfXz0187t_X<#$Z|6YOwSB`m(An(T$+L}s!jR8` zn)t)p|JcxT%WU9M7H4?tn=$*F+C{E-Jkkk<;?R|xVCmVs67&V#a}fWjonKn>;KG3hNpVGc^s zfWm}G=Yu^?_7dQ@I`c>33HU{u7<_)2L=2v~>KxHSknzNdBEUM?K?K%jyk$Z*;1{2T zy8*v=A$S4!MY#a{t)63iF5nk4`7;28G4KunenI1cy(Ggdt|{ObpaU%67hIO^AHQh9 zKvSm*TF}s>>9<=FA((l73;4V9uE@dnv3Q{za1-YYc`{HzlP>@)X-IYxsDNbUK*a(X zc*le}NPhzr>!f_3!a)jZzQ^sQFu;LzLHxgpvwXq7y)b+;Kw+lyz!e+n_{Xz`Z*aj?`qIJ) z11j#Zz%>Ei;X|0oBp-PjB%bnE-T)4>aNB=U2=24T zMA8TM+`ms!2lqIXB8i2&A0yR{xP8@gQ3iZL z!yAza+?qNp6u?cn5#2tJ|BL~sH&b_K@&3j5eI#!rI{Y98}m!8F=FUO22v zC*fX#iS-)pa#*9`3+FJbW^6Gl74{9NI6U_WG*Kp=;|GIFhpbmktCF@^y|Z$bvaJRe zF{Kq&kCx-5$yV1^`$+?>(BP}22r-^4-DoSfbYD;{TSQKlS)}R)STl!OP3!5!1xkJK@ z7DLj{0*!@4{YMaIap=kf{%7+~w;+C|IZ9*TeKQ-^e&QvYozuPM>P%n0Gv)3v^%)7~ zl+F4%R>N|&a0Dt$mh?c7ta$NLXi!SFUp=)(dfLul$&engd$nk*bi3V?y+oA@k+#j;Zo#N6TKYs#X}e!NELdu5 z*EPbI*|hh3<$Kv6HSc(rZDwlqJdZ_fy8pPsMTg(b;@T`69|`3|E_^UXVE(qbI6f|; ztpr3@wsJ)tB$sY;flYCg+BwLQ< zdz_26nM$0TQM-4D|2iLzEEeB#4v$rctDOUqo{JIA?&*KUvz(o?EkvEp_W6rNC!J@N z28-;Rj8$pEZl_lbxxyr;dsMt|l9QS%6i}QP(oTVgQ-%5-|F%cAZ&93PkdS`TIpDQR6jlqBh2 zJAL9MiNgJ!g|&p@-o5aoq`FWoD*4v5w61-s{o zE8H_8_luG4hhhuG%iQ-S@kMXk1JiGb$nM@*FGPFY*XDf@O>|#Yx=1K7MFVO_4O4W^4_+!LG0nZX_rns-PUJ~wM zzSxT_&E{|SDpi;Bo_VEqHS&&oZSJ9Re|VW_NZfOt{aP01mnTWbB~-Wxb^ zWv|#V;QRVi(XRmArcu%Tfc~xfMeKmPyEvk=0aqiwiM9tQW0#A}1Ncdy!UqAQ^kiXq zKy6m8&=(vbfE5@5&XrsekOEFrz7+%oBs5ImzYW+=UCcip5W?NWw+Zl-?&B!~T-3?D zodKp@=eSz`S3QN?IDe(4fb-73L|e{D@{iP2v4{K}^w-$oe&0rBvIu^?V+R?h{B6d^ z#fn|vJuIg0+6m1R`-ghN>ctizjkDf~euw1GPZ14;pq6@z+Cx%S#)+y!V%EIg!NGT_6M1iguX62p1;GlbCvQP8OTC9H3a;on!u1S}?MdZa z3tp#5Th^ zE?k8uizpV1A@X9b3AzwjiQfeIi1hTif{lp8tTp`ii0HfkJ`J&}1jP?WY_6p7{vzD# zyLe=T6Xhk(6Jg7RaUUZrrHi7ZTTqyig*EUXB`1PJ}jzxHbCX$U0PuIq=t-`nK zl35kui$N|w9`?)dmKhlKWNZU{ci0$EF(dW>1PTAfPKKrlpT(?$c?h}DAu~FJHPOrG zW(%{U=PWh|Bci9SNEWU_j;-4*_>26oX`0{x^1q-90tWI)=odj6@x5ITA(b;GRLIa0a<^ zkcT9{I3i@2+M45w+|uRD?nch<*}&cv^+n^u>Wk{u2D5^rsJaN|^{8|D3(O5sk%kA1 zoG7ocrL^WKP;D04CCPyb%cNt_6v4MdCiGVDC;>NZso+Y&kvVGxoP@B&s|CdgelE`i zu?cI|J{7no*!!>rlM?0zZQwsnFc0PMsR`2~rt#wwCdTaHFN^=1Sju}9KbofCHN}sd ze#8rnf1mr2H!1#k$r7$AzOOQzdnEp9J&HRuzLnC*;l#5!ZJeO^21!5rS$u_Rgq;$f z(>0y-Gk$;1Jl45*SB)L>SNwFXD>EbRrEU}BUEF2;DMnOWqoJM7j6;u^Q2&bqEJ?5? z9psw=r}RPyE|{Bk7mDJ4O}#ad%YTtdwyxq|Pp!1y$7iJGxcKpNQje{T%TgB? z|K{|k&aPa|DM+1OAI5P^`As>)zLoNx)4)EG@<<|Mji%gG-D8!eC_9H)mML}Jqs;o0 z6B-zEc1p0;l5rtrxo!#lcM43upME@f&_JcBle>&xsTRrYAg}k#26-;uH4Eg1{CQcc zp}D-V)AQkRy!WUFRwlgrs4I5IcrujOxq){9)!_b&7mYgaeUG;Wg$g{+gP~4@PUYS~ z9p9J5twbe6_i=ZljwD)gEl>$*dpHkJDW`Ke#i(Ps6pjxn7WlE@`#yBppIFP}{t;1zA_TrofT>4Mk z0`e1j6>chflx~mxD6ypRuzjjkv{l%a&LHYVEV=s-)gN2hhoW?1lLxSrb=cq`CYg#| zIsAk?89U3cjI_Vvqw!4hj*4r*L9v<$JZo$_;tJqE_}S0`O=BtQ^|1A<#QG^SKC`wF zUM@&tIT9W?_%g={U9M5gJ_6sfgGnJY_+Mk5AXEpRXZjM#5p$Un3B|~I#!W(D{0~M2 z;X>*z#vVc)s)k`fAfYw%E<$bb40ihtg>gPeu$9!Mm8zzc@*NfCJ5u%fvgf5I@K*&M&uxWCB> zzXCWYAQ~5V4?fetikW$pHWix0M1lSja;60(Y>JHWnX+PD4WpN`Xt@W2L7BOxiE)bj z({nMypZvyu3&V;$7;Hw@k+tD+I*0ra=}kXQ*2GikZsfN(#A9OeOy$A5~r@6m<6NnpNo@G&*MTc z#(M4}XenLBX_;V6FX3d)sienq;+8F-dvU^6-=|OK_-;H$8{n+;Q_{E`fS72R9Q*KX zG;hv=sOQwboZ0aK)GM4hDGVxxW1IPf8o+Ttds4n~Y%ymj4IDG9lH$o8$G;~(W`88x zkx#OR*xSf(_8sw2QaM|$tRR`Qi5-IGCiaD!_nMcobMAd;VzG}8m^Lk7BZk}>F>J5l z2KSY}+QCkmE zzeyNNzEHa*b*uhR>m>Ob?oneU>3$B>HIkz{Z79DbabZs=S0vF<=O~4e{c(DVzXXvI zNSPps$i$I5B+)s~$*GbEj2+ohvJHEL)FRn{Zzk=MIFkCCKS~y{Cp4ExEX1put;K(o zkxfML>yGTECE~|7$&Flb_r1%FPGV*Me?*d)GBlMqRa`v0rQw)3`Ac=ZM!aM6b3I=4 z8+Z>G?MYzdCGDOdZny|)6;xr{3qZY%>NfaNN}U?A{6k4m=@v67n^pH$&7(|LUD+_3 zd|xHmf+bT`yd6Yxii#N)M|M|HqO3^YRgH0FB$=u%`8_E`Rh_w-v`kf#bFTTms=TPH z8KpvFr#D-x((&7yB&x%tvZh_C2v%F;dsV1N-yIlR3|kX!E7e~T3GT{<(Qdp^c@TIHqOZLIBlo@53aI_?H8&9V zTnz!#8+&CTOh+2)@-)9m>g}3qzkx*QnznKoDY0|RV;{-6^WElm&Bo5xJ7zRr>m1tq zrn#*1=|N`m&d$eiOPZ&4YLc-{ot<}1J#NbAyqROuWZT(Mbf~ebQ&u5qjOk?7eJ2__ z>q$ODLMN7$Ms)4Oh^P&q9h~A>!@kZF9W&}*bjIJ@P#@D7dG8=W+qu2}JRzvleNccG zbS`-N20ybC_QkUb#-J;8n@!z}YHz(sybmQ;2)ako1{b6-s-RXmQwXNOz->PdZy0?6p zRdc#~$!KQHj+>ySNZS4=7>wNU5qP?rdmr*ZT=8H4n%We2zZJH$F;r7EgV<=PIlkx_ z@r@?d1xDm+_N;3mCTaY9>xk<$KHCO}aE;fV8x1!!9tWrmMH=@bYa4tutB=d-ziM1g zO{=G9mY+LZzeBUQP((0j7FL)NDm9jM@dOvm^yZ8B>l!$71b?t^Oyp4azR#dYs>|#f zzD%w)^gX|!sZHq9+#RhM=!WoNzO1>ewO2UQ?A6Y@98-NqJL^VWwT~8l zx1)+Z@a^%(su=^i!6lVR1GnEs;VuufewO1F3^b0~W1kNs08;$mbtb4dieCc<0x{&( zYrtC@UpWKPTK&=vT14>H=_Z)rpXrNiKR!h*OWjNJt(H%cqT{EmpEO=d0t~*>FR^y^W)~c() zQ6tSZ)dzKZnFp#q>vjrjtIq2DTc1~s>%1<{t30i9y|D-PMz{QKE-pkj@392S*UfnG z2|IDf_|~an|In+?=gZ|ocYb~=&lnNzt=*&#!URGxyJ_eyV32 zYOgNQ(~oVhUaO~^Y^i#tZ#uiQDoZ`C9f!Z})ju znX`WS&l{x=hCxn?|NZ?681tj=z(Ije`o0B_R>n6X;FagU3L%S{c_Y>E=xTR^VYYSE z7sK;q->Z}ct?SpS3`3uHUX_R8R?wEpzlNK;@+*~wn~}K61jCI(TPjTr*N;BJwHU6R z+=Sb2=sY9G{x-B1%)~YsR2Pb|o`#khW5s=gxGAL~&cJ7g%SQ|>!Kd=G21@JZ3x5su zmkKUq7;x9Gl)W$%_I@whY{+`-QrciR{^DfG2SfCmmXZyI-JjEE)e> zOZx*<)SmrA1##w|K)_i2e}gw*^}=5rkXa>vOaeE@T{3>LEXJKN4lF%@^D{nN^9%DHI>nA$O`;FJmxR+-dJI|Mv z&o*AVFzZ5#QC(AXA=KE?1TXs!l-7gFP8#`wlF~m$R?Cah^G3?0r6r@r`s?u}hmF`? zV)1oj!K3@di;P(>7GhG3$#0NFDr3y2D}{fIdw*^#JYftPAFKlVPykm2ds_gby!roV z_b=jBf;a;^3s9Jniobxu%&Y+WW$bkoLO@}hD$e}tNLJzdkHS<;1r+8=`3*o}CYB!q z6oyj{J|QXlf*eqoTNl92m+aUw(0w((wu}oXjB8mapfHp&IG`}Dr3{cgq?Lw%)ta>A z1K<#oO0a;!Y%5s`C=8~U4=9YZ*a=V=9i|#knB_%Z0EG!F@&Ob^SO|#Ej}-+vKw&hO z!Bai`>*XK_6w@z*r`mGqrBOg(ep~{p@g-|60czk9dC3BT+|RWG0^&t)2Yzn<_V#&z zUtGBeviY#yi{}Bq=(z~qI!6?3?*YG{w}EFY<3!t9ut#XC8vL*Gf7J-UFFvRqfbUqM ziU#vYp%SbuX=d(DVHQb#feMlfWow10=k@k zwhsUm3)_oSrracpp?KDPFt&t0xBF7V?f2If(cX%D?EUT=2jk1ai-M-s94+r z<{Ir3Ik;j&?Q%z;;;JkMsE|rQwlVrxvhtsbOPiq|lQS+Yh0s&~T$%taG_z}e4w=t= z+|Gy0?65XE*^ZWrE|A&Y%WWSavjeZ%nBa7! z0Wf~EW3St0L#8LMsP90gXB6sq$n<=kdKzR}R-$TvOskKm)oE2)DBlUz`WGb!cq+47XWr zY`Y3yxS_fY3!k^Sw=D^7wcWYR3vRJzTiYzS*?xlh5!~buT}^>cJKCo{0-to^uG$_B z&mL51V1M&_RrxSusX^rc`(9;G_P{R^lij(VxBEV@pFsUBI-vX7`LEY9brsQfJs zmR2jjT6k0ml@yD~4GLwT`F-k5#ar_x?lVQPd4lx4VzIf0dbG98>}}Vd)?l+EJ!35c zrh}UIEitA@?XdjCtgE^g@&mIh-c6I;Gl?DvkWDw4K6XJ|V-9AQ_5(`*?YwwnaS-&V zO>DPs>Xx?icIKAZZK-y!MXqfK+h5Dqx2?1NzUoKYEZg@UXVoulpZR`PciG<9=B946 z74JT-PP1)_6svt~D`H=$r`n!9I!&dqJ#@lBRbv~R?V;Lf>zyB{f@~L;rYhyOld7=F z7@KzuMCByV7+0j=*zmbm729lzrH@-**&J5CYDL>PbbV~KT-4d~xrMgqkmgN`%OVr) zE4g5y5~y%o7zI?27mST;mp)t2H&!h^yy)}z_{DE4Cjk{FS9n46Hl*|M$?Ue(&MPc# zw=HnCU3jMr?rgd2rCR4aZIwvf<_z=jQDdD(eJ`qyI}LA}s19^`ynC~Hfz!1}wCbIc zGFGH&b>bxTt8$#`(nnRBoiN!mls}!$Eqfg8G!Ny^9JJbgd5lA&?!N4qLzI48 zw%5Ua#8+DBFnO#*yx4IIP!YWz%wBCH>*As9>hJFO30Kv(+!5yY)k^mr3lZu@_sz?2 z>ND={tB$DmxI3=@re5Z5=X*{KcemPlSM}6=%5GDY)Xf;VM^)hVHa15U>ZVO%s;u2^ zr(ad}yIsn9tgLiv$s1M%y0J=UD@NVws=O6Ux1xqUik)s5)Z?w6+zxYr<;ZQfl-TOz z=A#z0TyeASQnf_7zV5jsfA7lFsO6_!6SbYP->y!&F4>ti-}GOkBWt=wT%~K)@W#%H zdRhs>`;E+1Z+rdn$_Vu2>pj*Af+hW-&HRL^Y)m&A-*VuZ# zitqKo*IkA2(rv}6V!Zly^{du--Hx0r5+!9QiC&cSDrJ;cWfoa!?S;vc zC~kS3DH&9p^Gd0jrf~L(ZCKpe=e3vW+j`O~fP1jj+-sdQqebjxuP$lX<~6koFMsa& zq^C)q=E>9$Wq&+VwKUmz&-FT%4CV>xwbC;i?~W{zT;3=cJ1$D|^co*mfBCl;MlA_? z51FW^1RjDZReb?>P2Z|61}GOSQc(gVOBJef0qm8RReJ*(*T<@s2b67kulyU3w>4Dx zIN_oBwa>+!npRfxE7y*#DJucgr&W`|3n_r@x}>v^?DZLQk&jm4A!|Ej!`A zMq4cX>GxMxE=Bp>)ptrh`!$VBk*xDOH5MWK;Rjq5>Xo~I(?LCbmp?R9RS`NHR;}_2 z**&XG!zEE5nu7L(JAsR}w;Cn`p}9;L)wKlzzd6T?xuL!EYi| z3Vra?m@kT!U`^6O#o6E+>0S!2;EP!yt>1zrdB|2_Ftr5RdMLQ2vaQuDxVV0xMG<_C zGS(6ioWY$d{~nwuT`R8+-meapuL$1U6)n3QY}s>E7P50#b4>bV=Vk3FY0SjQ*yx@l_-!Y*rR>rTY%Jdf7Nh$$tTh*6<0sB+1&aXZl95AgtB!QuHIN zW^AqCU6^!yTzNPa`lhu5iI4DWEk$Bu3R}aFm_&8!JR~}8uw?+5d3v;^ z5}BN5(y|kYEZNcmM}}6W%Uh6}>#O8Q@J1EbMCzs5Y zd&E~(Zj}wk7t|k<)x>8~aIy{YM>$eye|)T@PkJ&wT=h{pE#ALtqNG0Fw#QPkByLo* zK+K8j)2FW|4yI@-7s_U)T&wq! zwx%d4N2LIWampm)DUA}IgqVU=bxYQypgVQq&J<+#h}v+ z)#RJ{eL~M!Qc(0N{hTc+^?F>OJ1zB%FJe9QqvS>Mk`V4n(L7*Bf-?#Xp zO6`^G^68SIGA@{p-4Qi+N6*&{NIWSY6h4KpdEe}Mk+m|LcL9LFyDeFTyB~F!L zQA^XjWFe?!rw_{}q88^CNiU=36n98Zqo(7&Nu4tP)-RGg$Q+?;m7K|Z#W^Zjl=(nX zB9>)dR?)>lnZnLXqQ{x#-S1 z$)2E#y|>Fm=(DW2kTeo~)1Z6`?*T|`e*<%_oEyz3+g`$7K| zzVJ{EuTLp>nNvD&M-ZKpFf_z}nB%9P$M?^%GsN*~&;2&4xUT1303V2?6mUHms}u(! zwgj+yS#t3Y*bUjf;`L^HnNP8`jjhbKc#;z&9m9NaV@U5|2E1-b>6n`VG-)QLGbBLj zhq;KjC7p&*M6Z+F!AKJFz>${1)J{n#Muz$(F~hXwI*PAh#Kp1VEDRGz6gy#vgdWi& zOa<8}I*ZBUEELVfq)NPnT+CiogwP$cwlhU=1+%cbNU*c$Z(l9Hx9G(HkH4+x;!qdw za#6#uk>^r`G6ZmQi^7ZzoXth6frCO+Z4K5ajH*Ep%d56SGo*=?;jkj9H?Cw>sni~q zu<)J~j@##GFL{phcl|4o;8u9~NeXZa0#-{Ra8|*ClBGCPM5_1$ZW{8HScIFHuwI;k zgHq3mU2&693ei`b3Hp@Wc*6OMxiuBPhX*+070(PyIQuHvjM?lB z6=01kArk*ND9VYT`XM>i&<;(LtgC+lTP2xEcs4^KHV|4Dgo^JISPpB%LPD);s5p;+ z_LPbD5K{b=;-!RyV3g<^Ar>)9)J`~n#EQ@aMEqNk4`FYry9h?uk2)p1N;rg;2~mW7 zn2$nR!dBc;!F7TMAxw}$a3+@uEC`F(V!n_tL#*L@;lC)q@UGz>bDY`*%nF7!q1wQYjsD%7=`Et=A@_%b8L~F@+J*`9&$(Q`s3j4{*;7LL@SsKn2 zo*?s(Zo;)>W_*p{8=02!MxY{dQ7Z-6WGOmb;6mnNMEpnO2CSZ+MJ^%C=g%RZCWr9a z$j8_jyglSdaUFM%?5k99lgP_D9&vt>t#AI|6p|jp3UF*6fj}hx~r_$9PYEJ^Nis9Y2cw zC3A?sh&_sS=Jl}+m}9(bwhqhXEn@fKUvS&lm&umgaJG=`!O^oD#rruZ_IV|mJ1*#*$9dZ#E zxraOt#0hdZAoyI_a>zok8n^kfXm1ieJdr%F;+edM2z9Nf^w_m%AMo6k3s z`0upf4N5kJ-Q#g39#JQG$r87?A)d2jWr{cVgJg9khRc_%%X!2_N>*X!awkh{u?IO^ zi76h>2^9Y!-DSTJe`I}QXN!l#i`dh|x0PF2)na+aQI?&UaTCWBhza+^%(dd;{>Kct zICBVMIEZ71z3J8Bpf6b3AMx_hceEzaOW-{SZU?+uu(o|Rp!Opd2_WXRwE<`4u$ln3 zTJ{u&O23R zTt0`ZN>6^tiBlzKI&y4PDLI+!K2<_d8#_xCiJicnuiB3HXDL)0NcpT#l@p80{GhTC z^)ri97D^MQx$Z^cyyK)rLjMqy7m@4M#ykE1h>hwA(P_#N%m zP^nNUYpGBvM6wo2Wwap*r4W^^#Zn*LUZjricWX4EavfhG=c#lL2+5+7 zPQ64vrDFZck>1p<5M87?wLK!eflF{>NXCUq|LatRA@!qQ-D1w6ei*C?B@2K2UT+#iZU;g- z$ZB6*U8ZR3p^UooqD@Eo>r6$uCrPz4BAxRkwH+euVDs82k$QM@t%+z=46NpbXxY8< zHIGHG)P|aU!r#wURlg9<6$Dhr3g1_>S8EAh)W5Al3q|cZRUSfK?>WRPA(fns2o?6Q z>6O#MrooSuSB2%{>nq*~pS(F%;UkQj$|xra17}{9?-V-!JW}>YxB~F*h*$Q2*B^VO z195=Kyn<|MV%taZEJF#s>leg~co zaKOYeNT)hU%2$$9n@iI-;H&;hlT05}aip<(2C6cpR~%HU&PXr1XH}UZnEvwu^YSJ@a_ENf;tW|bNx|Z!(`bMfec)JvwJT}%`LYkyacU5!$dWz2*jeE!%edxP zz937x*;?K#yMI@&{H!eT;jMBd+3hFOWlgf!e0bSWS#&wIbW#>xw^({#7TR{OR7V!j z^SGoz=1bz2n8`fZ%ZqDej)T_4Yi0Y!t`#N9OkcMZzL9O1{8H#3TR!7aP&GX}Pb+Yn z2DQN|$@dyCX3Tf6UR0g`z73F8@;C5v<>1#sNUNfL?w->5@{jTtYiG;Ra<-AAJW)=w zd|mD+#~zF-UnTE!vnm^vqrGFx%H{26Q_IfETQBY}Q${6}6`ez~|ro?F*cY$1QzI$cDUr}x+vdCQYXNreJ=EQ?g= zCJ!H&E$ER4ju{thl>5BCo_|~JHi>>dB;P-y{d||)a6UJWC0AYiTlw}+6&N%9&%cp} z{xJlMmHImkkjnmFb&y6`=|Yi`YAHeSLOZzho`PZMRC-iFvEY_!DDZZVO2!o3uGdSd z6`jXT@L#hVo^cg%}eipB@2MUjg7Co79I6xGj@ z3(<=5vOk5MisCw_f_IAi*3^PnMRxa#f+dPfl0p7MMG7nUxk3>?Q2abh5jiTz>r-5O zy)n;3;XCPyW||My;);y^%Q7{%aZurn))UH}TyRs>EV3ri}p z02D@4H~}b3XyGkDVaf}^?>s(GFbF72b3rPgFt!EYnzf1f60mNO^Y4H?+q(R1pz3<| zISEjhlII?P!Z7m$fWmyua|9H|CbtSun9Dg}44)Y})_}seWTOCunc*Y={<51B0YU2f zImaPr&1MeB)OBmv1AxLX*vWvxNZ8;nwC6hud~#5W1u|e46P6hSd2M43K@fZgGY5kF z4VZf%C`6s{74VDi43JsgqBD*FevwOm1x^5Tr-Pc;lihR(@Qcs1r{J5yQo%P>6GBY~ ztE?jx#h9ZD!PMYvz;v`j*nFU3BO5&P+uYb-7TyuZ0=wBgd8`6JVJIx{ ziSr0k0VvE%CK6DX|CnH2@O#Hp2NY(QAp|Pupbr+hmB-itR9vGofr@SP5THVf_8F)c zqa^|rjnr>I#W`vcP+>>;^ZzQ^fr?)gSD>Pm4CYGS)BjholT-;*Xp_JL&9Wq+vV2Y=ShL-Z7&g4Nnc`c1Yc44MPs8`|V@YLF&OKj0i|Qd^ZDhuiRQm7enfI ze$o>l^#?@yHc0(3mDUTX<>k{(Kx(B4)FDW%<{&i`QfoG+d;&X6B}y5jieEZ&?=b@;We~Uu0hxWtxz1q1w*Qfi>&SyW6(Hj z$r4i~2PSxuRZlbT!oIG%!1Ra7H`_9;VY8;YnJZwk7U7IB*o=)D0|EPVcn>21COewP zaD`2tL@;z=|DBtqPr#%XXX#zASJw>aQ83BP)u1PN^v*WgJDBhRxJO`pkBw+9Fjk%p zbp}Q*)urBt_13_s+hC|>6$%d4*a=ZC!fNmuWCg5I z4m&DMA*RE2$q!y%Ix*04reJLVw7?*0JzZtb$kQ@iqrynh z+Mr*=2-DIu&SxCa(%5as*rv5?KZ(AeIqSHMF3}u45<&0Q#GR<8Khdl^`;qRinRRgu z-CXnfReRdJrpFC9jiPCCJCv5B3EhvN*{vSR%%jRzSAibs`qh_9dZ;H>n^ZF?3mUQ} zF{MJIy<>`UP~#5n8+lw~EA1yaT0NQfoxDWtt>`@|Tg_AQmb6y&nRJ|3tkU*DiKwlN zo3kgtm5motv6C8mfeN>+;BI9$7<57dj2oM;u2^KaY}V2CVpwfns;|w^-So?N8~yjD zgOSBk3KR-XD5P&)pebOp?l85&vB}=1D`+CpClf4h zl=RT#sz^xEGT9~>CYBk$mkts)8zW`E2xZ0*bGG<5#=Z*=u_-2rK*cF*@boiY?%N4% zrE~UdRJuR^I^nrF2uMM^zpwIx%Xy39&5cOL~~ z1(kSEf-S#Phf-85UpFO_(Ut=p1!QkaJPt|vWLZJ$CS_U%@^GZBmYYPq#4Zb|gh+I- zsFRWibc-n2CxWAe{oGD`s)gA?9CnAr{}vY+R0jl5;b%V!Y0v`?>cd9qJM0Ln+vpqZ znspoLFuQ6a9h%gxXxDNY!S1nj1ue%e`H&Iqx?P<6eVV7;HSd0!q1}bEYp4pl6G1Li zuAOsOI5pqSIy#dYU}uz2L^ZI}Nol0Kv{TL)qvYBC&4nl~w(^pVl7 z8_m&Wzx65F8kg-3uc@;x2JVj3ewX#$dDMCr)iYA+Z5LS3TIx~fA7O{6Yn)}#ew0_v zFA}a%s+{>Lu@paNVnzc+)43x@L}obG7k?pNcP_0~C$DkNYT7}GenbF-_NNe4`m{saK!jYU+D0`c7vm*$cbxH#OI*)uDiT z)vMkeMs@P4^gd7B=#_t_jWXkv5jaI5dnJXfp=5g9ir!1{_PUbbLeca(mvVwU=;e{| zfSm4iFb74p@v zU8I)!-c-}2X81a7aHQVwwc2)*ddkOb)~6)H zf1in^_`v4^yD3}YvMcY&AK`DJSCTv6BMG|Xn{ZyrcCrN=mk~hv0B_EDLaKyU6t|E( z;CWR7;w=1e;|%dR{Cf1 ze4fjSaQZ$G^4Hi%pQ8)AdxL#|Uxs=n2xJ6Qk05Pm71cCQ1!hY55g<^pr@RfQTyH_? z56IfeptJ=fn|n~w17h~=rJM`6YX60@JK&PrEy~gWzvKTSj|RZc>?hX+cn01j-wbfR zf+imfu!|CtVF8x$Ur5vd;}jSvIY1|4AIUagWlkt@#{XAwIOa}oPnh<9 zh5krL^B=~oBpCS9Xq)j^|5DyAyoZ0ZXfICU?=Er1UGg`O9>>c3e#;(UulkAQd<@YK zx1ih8=f_-Jq{M}NfgsA&upDSP#p_BgB%mC)ylJIB#VmAk-CN3<&{2cG1nl&~M%tH;(96Uc%2Rn%m4P8N9 z#Qh5SkEf1%8bTDU!)*+CE-}GYg@jA3v4$b;vItB=h@rfz_gBdBg=IbFAxnWLr~C$} z3X+SWJ0KYONfZMzCP!WmRhlDTxV~fU7V^>S8x0J}=GRy3&?T>k{J!@v>1E`5`)#C- z$k(o1Qfj2+xEskga`-fwWDwaO2qR8Kvai?^nihVUt}=Ftm6 zW90K3b;5*~wmEM7X@H*EGUQ@wxj}1aN7Qx^9F0DXH}~*lUx@6ZS!x zq^EIQB{NcLT(CBe6diYbi!I3~?%?(ql0}@!9x`cJ+?s>k#FufaT*HXyxMj!Yh{>^w zr$dRJv5E_1qGs&Z%Zmh2?B^&;LQd>t{7J&G*oow5f=cYjqh>rOmXke!PmCoLE8};> zwpZ=LiDRo8&*JXK=Am!nw#KGmOR=QbSZXi!RID$Ti}^2hk8l`sJyu;j)jJm>n^@?* z5yO=2>G>1`4i@Wik4aRFbYWwFvy4c(5AtXt;l3$|y(z#4L_{T9!{&(2COI4{CV(RP>9vHQq`ep73C2mgE|c+7 zNv7Ao;p>yO#jnSQByCN0z;8)f|L6|xMUq-}H!dehsYr@*O#D%$f&G#wYqY`^CB8)a zV)rNZW8*Lbi9OV8%%#LaZf)z<$$KnC zZ>RWPGjLEyGv5Orh$OQPMAknFLuh?83ZoIK9>G?p5NP$f9;w}J1jrk<&~zdDC(F4o5n@_*E~z$p!k#v zb}e`bcrT0!o|0n=OTiaqpH}z7-^;er>%w2i-fF6Wx69VBoW*NrE!geFO=rnmw&BQG zZ@orvSy``qgK=lF!~qhVe%8=sbL^|E{_9cLvMgR)Io3a`FG+~i&Z0b8g<)o)vmG(< zS@lIR7?Z3LM04+O)-$BA_iomG)Q{ecSy5Qsp0+GFbzhHb)?x0c?(wYc!pq$kv(&`N zU2mU#o+#-GeAYib)H(jFS-z!n@3TjWc=Y#Y!N7a)w=@^*vA&f8?;GxaB|K;quA{gg zW{kUCgi?vZT_{S`HODy?#hU2i^oxQm9k5@Eylv~SoT5X{&De^f176|Ss3I%hC0LuH zodKzszeT2@uP~INt=Ek)_lh>hox|7{ZB2UK`>jYPeGs_FS7xv1JyAGcbfD)C=)s8U zX)K&T7Wa4;j-V*rp9^W2$?lXwBy~ymy23*4)~=?)RH0p$b>VgKxlT;s$%)v`y@giO z?H$d9TC;yURu#-Cd{N&E!1?0X$(j-1J$O-51tJNYy}cCswAuhF!n#x`Ef-=<5ThH8 zV3iSUF`R75+WtQw3myx6PnqZfryP1 z_qri&#;xvMfrw8!)k8*HPtWd&L|n+~@6kaVFI?;G)~0)(k<27edK#il060JF03n$ox4w~Yom=X#lqs|~;QRy9JqJ9}f1 zpKJnq-I4!us_4~2j(R@q`HU38_w*2uJb!-ALnI^Avd0-oip=OyLK0$G-3%lxNxl0H z5|@6w+YH&1mEARjEGcAn-9kR9{N1IEj6-he#2~Mtj&`0vp2pnjc!PAH*kc{jYZKL@S4gyS|{8M(*iqLo3C`bw#09Br>~jE~&<3ZOhte?+`&IPQmzfvb1>m72t z^C9+V#LLdZSl8Ii9e=T|iIE-MSckOEj*D1}XTLgDVYd{5=5(w^Ii^bx)rQ5?jTg5$VDjH^T8Ws4(<@r7FxO`j4u~Xu6~R)nmG?7+}`zVl$RN>Rr~faoA`lk@`(DxHFeJWB}_7r!sfK zI_;_5`ww)kqSiSA7bUgw=(mnmYU!z=j(BRm-}4R!YIcY=dXbtLk%T5vQ)4*jTh!!4 zO|&sJF3kruL=AgZgG!*DFPK8W=oLwkhhCUToWa>RPg_Z4Gr5 zN2j%p@<(v6br)r3JglXIGV!LgWj94QC26js5N0i!H&7~m7B=-$z#P@Jre6Z8LBIRk zK*aU$1@Um-1wg$Kd|Oy*$6B7TdJp;o54QOVn!)|H;~P4k`_4KX9l#xNG(%f*g-6ez zA#UF(7Zjh%@zX}-aB0C6sFPe`gf>cz+Zz+y&g6F9>u!(dwx<4RH{{kn^KIjE3ks^+ z!no-bAKH|-@eONR%ehzE54YNI1A9|ihPb}uo|Yi42m5{VXRd=ltvQ}+Id0RW;BI?! zy(yNfJ=NYg&G|FCtnmWp?a#LbeFe*y^B9{Pd?S+!+K?k*W_J1OzjJ>F&} zaW|`L{VB2A+ull+m^;v0vn0kxj9V{A3{RT0no11LuWI=u(ZAHt(k59SzOyAvqJ8UW zi@rqT?w4kPL^;)=IZ3koDYDsA0xMYF#1sFlxY!gT{!pLS1c~3Yu^Y?9Q6ovJqx-E8r4XtZwlA@FZE$5`yHk7sOlwLHAY+jI_+e2)oNj>d< zH)l&--BX$arOqb`o6V$-=hB+KNbN5fHnmG_u5~qqN%!5_(4;T5yqnr6knVgq-k2mc zd+OF`DmBV)MfORzRA?fvN_FZzkjtd%ZTB0hq$_&*8um{tl2_EzC%&@}*Sk%87>ur? zPrMncuCtyPel4wSo?uRGu3bNYnz>!`WTI&PW6jZtC_sw4KVJi5F8=He;*n32fD~gt z0Uv$ykO;ZW+8B zU+*LHu2a;_%8s_}t9vXv-1D$*i_C_^tZkOrU{_G`{nlsV9ez2pc>sA{(T7`tz+N7&yByo-iEZ0GjqX8 zzDPScZ7mGI)mb``{dDX#yhKur2x2+Ak<;C7t>J{>WvkCQN`SXkB^|$4@ zR~hxYfO*1eHGy;ED4CC_;9yUtFY`s8-)fAW;)A8XU(_sRon4dwB5H8phk&DOUy z{_=>Pz14H_5K=<*J^6VSrs|LU#K6a@1i9;&0pg?F_Vrc7S^3V%mP)F8(~L%?xm;!b zSp{$Q+v4BGp+BH*-PrvHJlBmE|11TJmH+z+;FX_#WkMSDFBa}At*qaoU}zt#n^LqI zn$=+xjTSw1DT-RVxVoc?O4q=;b&B%iq*|$>@Xb#0)c;HqowYDNByjv9jE z`5oh$%Zls=i8X5!PoFHQCMYtW->D8#q?Y}xh7>7v_Epu2d#x!|2Nm(%V~AnJ4U#@0 zP!YxotbD5o94M#^RQQe#R17OTUazlkR5(nYD@Q7VIkh?=W~(3liboTWe1P3Ujn}E1)p&8ql9| zj8;SbM`3F20t(Yn19o3w_p9Fl3L~xtwcy*~)mDJQ)Kz^36y{JBU^jW4RYw7ZaYoDm z3X_HaZ{Tj>BYeQ>Wl#zFZir!(b)eqYSa}dom=_g-#l_JT6~2JNoGhmU3UjXKvMMJ0^*e7-k+updyE zr-RXe!c+_z0t%BoAOIBR`9K<=F!u*60EM~U51y-t6r41hJae#0X>fZ$Rz1%>3Mw*44ioT;B#srJFa~Up zr;7iwmjQm!!ioU=qL~RO$!rM|WPU%1^!k7NLZ}SER3imo%}}oufc0-pv;Z(~-9EuX zKw-oJCqQ8q2B(3F)q^14I%qv;15~&V%mEdy1K|4bBLnAvio*jdfr?%IG@xRAKls0H zP4#aEDti0qK*jaGP@uw>{}-sx=H~$wFZtR)MFy`Ls0ih411eOxKoL&Cbp|ThIp8|H z%>P$m!L9`qMuQEQ%LJ7LGNfrb6O41Vhq(!;kkA2XT3i&If{a#v6zqqzRbmC;z0q2x zgWn;wjaGv+NNsE2;C)DKyI{~7QnQi`01vmF0~ooQ^NE3ANX_%cz$Qq|=SKeoq~`ae zzZg;rz5^T>YT=3f>X7O!cpnE+y?e0l7Nq*%Pv2%p^|69agH-d_{6I*hw2mi(RBA5r zlE9&K$9T$+O6M|e8KjI`D+5|4Tgm(i zEtM~2T0=^To%BRVd2w;@j?yCZd*FqVlM-hjA0|@s9k>f)=x7gIfKfO14D5rEO`8Um z!iW}c`h_r}O;UdejBvQLKMICD`nBHy)_w9vzXq)1yi?yetnHFrUmdLR+LgX5u-cm^ z`gX!9?_A(d!ipd4<5$6QAN%l6z#iw>^M1n~mfG=(VRviR^K4-^n>TaCu&W(ExHn;e z_+?y8*a_Msrxs?z`yXcq%s^DfM#Gj$O4vIfsq_k~0V2vCvNl4G<RYSv;^J!llm_am4ZmCC;f)jgB#ra8FY=u< zw%-rqFIE4L`HV+ZZ_lmg#i_@Xpm;{=R@G$gu-bH!h?}TZ+VP6JRm~muiNjJA(Bzy@ zRaM?B`>)C&(HnN2@=3`{_U2VPr6N}2ibdHk){f6@z%dD>8=ILE(>f$=+*>rF?@AcGhW1LJ~0Zq!;sq4p^V)t#F6}@6# z-1tQDk~O>0NIJ&4vjOt~#)56Inmf$&TtB(+m^P{lvcUnWIbg*DTX$TCa{7d3>dTk* zAxv>vaecX_je3vz?wOVtq5A?&b9O!Ivon3NU%zjyY3iX${twdx_euV!Y1oOae6;E5 zv%&l{Q@5Z({u$G~VKly>sZsQQycyFq2?}11DeV3#UYyD2Oe>zH$$0J=?gtY}Nf@`x zq`5kod&H!msfhE<vH|*-zuMCyl3hB6gZ_k!XOeZ+uE3V6_>qk@m4{ zj7DYiOo~y-+2E(fkBUx%V93$5MKBAAh^$P*5=Mwhqj0maa`PoD$1@ z9ZxuiEVtq++5cHCrM0pjTX1Dv)ugZwvkj;jm!LOXNahkTr!f#FYnsh!Tw8Gf9d zvh`#BF+1qcI=;E>f_pSy*;ek2=S|waJ-dWQvKCl#d8W2436Hrm zw&f{h+z#6(8GLSpZBoux?pE8IC2E`z+mLE=&I4QDrlTBl+an#9*h97laJSiUwp(bA z*?Kl|UOtOtgBRto&e}YblrcZrcu5h=+cr9~A*RxS$+@)*p99>5OVmF$Kt_h1O> z%XRC3)cAOpepnkn++~?YIsd%#`;9|<7v~p-$$TSc(ar)sU|&YPOHusfYKI}WnXIWNJTX8m#cM7zeSaia5LS%;kpMKR3( zIbD$?F{7QVrKya+PRnH&M#f?Aen0xl!|e;lsrgQzbD_`18?2f91#c5bm4EJ74=j^6 z>s6|LjrYRq`bLPy^18TfhS%tIcIS591Fz%zWV|z8P7Z0jeO`8MGdxYNz25HJk6zo) zlyT`^TLQG7P%h~?b$9L}lbZ1?08)7dXQ zNmcLJk34&umar{6TRRL`!~KW#`xgzR(gYR z#{(m)V=VP}D*r@3>v3(tp1Q>YJl%Y^vw=XxrZfM8lzG_GX)u4@Mc)moa^5kxbi+EH z4P3D81WymnH1Fol!}0qvxdU*t!#-{kyxFaV`v6|&t-?JIuQ(ITwSeaZ)^e5LnODX* zLU>Bl0;dcflc2%52oFoq=WK(Y%Q(rF!aZ|R*ro78#Z_!C_}(feO93};e9x+Z8+I&V z9f5DaZDxLgub}N`KJ$6ab7h+Obcv2KXg*IQz6@WVQ0WEwq|YH)9{q-oj$A_HoSI+Q zPHFHlSX|_V1*L=49v-9yt>o!kn1*fTG6RxSR&vn+uIra@D+2axt>)ej*k*3Y^$%FT zZwuEdK*RnOS1n+bTM%b5V99YFhv5I`j4>z6|9fCKC(!@Xl^V`Y|94S+?0Nqe@o(8g ze_`@BcCtS`!;J0V-<5NY_0u0&e3#YgUshGcI`5y?NM|kae~f<1Z1TU4U1WOt-=J+^ zeDwF^nKB;vZx!ukZ1P)>IMC64qtc^vN52kPBCXFaOU|Mh`9&|Rr5N~utdM*4suBco z1H($7<=i7zk{~V@ke8L4xl2Ox*L8E`p$`mRa>hb$>_BijLoe-Z;berKwZF%?6ne@{ zgJT!!b^HNGBh>x$B>PRMW1uCwE!5`9HTIoQ%cw%ObEs*22YY4c=41}5FI4N%U)Gb* zB{>!>_mCgOaON*?hRYphOURqXa^~rfF*J$sD`Wusict{)45o~OA?3VPba61_X%Vwvl_P zd>9`ijT>VbC6T)5BF3ReEi8^Mja)_@r6)&Ba;NAUB51-{8akp}vV?XhB38PV%8xi9 zJ4QVju}fY^K}TpRrb%BSRsv5>_3c;)VprZa0I@J(FQm!Nj4M|%WGBWMXjikt<5q3a zVV{isvE7et5i8x(#9kFEI9S8_FP80knuU+0ACt1OV#%jZvjSqV7tkz|*v`venV(}@ zql}pyvGwt;%$V4U>);1Y(mjrMp$fk)pmwn?1e@zdSC1b^mTe{ ztSdH`t`mEJilX(zZsgKw$76m92dMwWjEJSw8!@Pf@04FLk7c_kNim`FJLFd}E(!tZ zU<{}pu-NwtA&AAg?+PM48KvNNk?ok=p@qtCKT zT77T_OC@QU%PjL<5_Iel6O;J+^fG2z;{1hcOz%Yb zGh+@?V^SVrVI(FoowM4X*_2+T|CX7Ro?|-7yqLK%JdZ=A1b6ffumxqjB=_ikw zFofw|zU_>P^dlGa7+2DrE?;AqrysnIq<>DgjvJ)6q+2A-(ZkXWAMKzUr?1bxLYqie zD=MWGro#~ZG>^1z$XV(_+7x;nwJz-i_8`?gjZgKXOr>>lV=2jL`NC9+X4)-r8M!p= z#6%Z)OWMxqPo%1}RdPGhl2qU~B($VbfP><1o(FJH{LEVp;-_3MNQLn_M-O&~!O7mN zZq7hw{nX216lBRvelTLPMlD}6ytAlwhK%i59WI)TC0Wg0Ou9I$-WN`<&#DgSqeo?x zUEWOJn^kZ_i@E?X#lkK4RgsSh+6Y z_p_6V7<~UT;C5uFm4ZDMV_7NKMbYO=#-))m7VmaB5&R0^sFLBlO^=X zqFt6d>Fz}vZS(0{ijf zvOuC1UgmBjoiBt7_Yl7n+KW$u7Zq=rxK3Cogr*w_w+kj_f8r+#$O<2vydV!aC{$~{ zg7Gh{A%LCYY|R#EDNR(p7RsR2AjX!Tp*=;^ZP26LKol7Lpq)UZT3n-N|Z+khot=!@K%@fo8+&E zZCPk?EkdjC9r+jnhS*42sQiI+AvIKfL`9K~SH8wn5WiFoP#DA~l_bt2(Xg^zs6@b4 zmWvGuE|uvM?s!gR`1Aw3TjjCYVH~E?T(KKFU!edFisdb!ZbMyY*#qLsX0XSlwlvj2 zo2j=N-IV^L1|sKlY^iR@2}23h5GmY^p!`9SZM-Q%$abe}N;9(2Gmesktc9CUjv*`j zDU|id!cb%KOJr_jBDoCtG!{cXjeL|SCu<;+(k)1INp#sv@^xf(gw?P0C|}D^iJr!oO_zDj2{=H@qAFiu==5{PXT9o5xvJ#wPgmzq;LRp12-PTN= zL~q!|C6mxA4%CqIP(KbYlCPp>Jr>CJsEf9Apw=%AS7%=#kK#ylTcw8Q~Vj!X^J;a zj&kS3<1$b-f?}L5YWp|^+l1Qi<`dQuwPM;F)7d^Zn~E`QANl#N7tvk?ya%6&fY*~> z6JCMXO#pQrawPs9w3@7q^HEwy8pUR7KPL5JuW$7uJ;w&_{Eu`Mdwl;i=^)nra41O| zyWb;@IEl6LaV7R(clyl}ld&ctQA8K4LBud|8CExT9f6M3Nem#|#cHJ05O!i=&pzW{ zVCD*U;ZrbgDQ}EP=J2d<4-aGZlh6~;3(i#|37K)gbwFXdgP6l$fq01iAEZtkruD#t z#G6#1<^iHNwROuBaR;^1{1XwPX6-j2h^R@9bOMr^aCDk*hZ=K=LpVZ>^2;P>Qm=+6 z~^m4o`J_R*Jh!wJMmvsZzIsGuK+FTA-`vC>7Qo zi+M|#?QO&)Q~o21F&io4oaMb}N}s^I_b`P#ezs?Tf`0R~2TrM)>hB(<#dC#wBtqf(vj?+@c5-oDw%L#t%#8KE2n9 zjpL@Le#e?|?>#$?8R141lwuONK^5;X>$xWy)O)+Rj_vzVv<#>$pneX5G6v-`)gu^>W@%Rd-o)MrZzXc5_HS!#cw`&w%&9cm&j_2wEek zf7E^iRLk)(!`Gn|xXGbYuw^(`(Qf4;+y>Dq{V42r;ot3aEKfMI?;*BO_~uX~HeC3> zBRyCLq4;DYRz)~+J{>b66a;U?6bpIbrI@oq#;rMwwvc?!tG7>xOD*Y55q3ZQ-D@sv zE;!jE6joGJ_e2Yy)eCx5g-LA--3`JUz1H2%!pr1{u5qCsySgh>cw(@xb57_yw$OP` zX#HkyCroHM72A<4Tt6d1{}Mt!521SnLO|_t|M&~8?xhIC%P+xvg;o5o1%hGEzd%5i zm|Thf5;i74vP*Y2=77Y+EDNI|*|@i&cU+?4(B0cAS$1S&Z?a^`NxfcA33P6;ca8Y> zrHUT0_(!-=Pmx%DE3N0G_|x5~o>k(h)cxHg@qbUNy2Hh<^B21{#UmBxyE??|`X^n- z#JDzm=d`%BXQ4AyTtnW|xlUZn4)3TDKN~FWFcaS&drWtw)bPaP?r`bWb9cMOu=9m<)l-MgOzE=x`c4ZeRH4!_G%;WA+z~bLp)IyUd19gmkFJ{- zC(WbxO$@RvQIrYBU=YfA0z3A+oifq-dZhi}MAg*Vw%&=HnOkiZ6SwC<59|b}Quii) z26CO?%g(!TI-6u?yuWqE%Y4s%?>r$ob779iv}o#JnkwU)g!-k(+CwQkG@lmGLT0+_})G&zw+c}d$K(E`TKSwd0_eZ zHm=;SuDtEC{B-O8+Lp+@dv>;#%iT#gT6f4DSRE~0@;w9ZTK34z#&nxod zll4tc$#|TKS5bkN>pBDy~0|Yu%^_FZsJTFKhP0vCSmDj`Z)#V#4&*iMR@ja9H8v_NU$1N2s4$)c*??l>7h=PlLT%o; zAyENal%aP0V=k!8|6)5}A|Qgb5JY1TRoa>Wg-L5a1weNP>1hREBrLTRP@Cvit-wikJGvFrQt!96fZu<7q$L(m z7($C7pfJapL0zvlyBT<_TgIBrz@4$XX$Z{zaZT}H&p>F>0QItujey9EZfZ0I6ea|T z02C$rm83$@YN-V9ymj7^?GU8DO*{c8%)A(^BId8e z|0v9daR%TQKgUBL$aUrTdcZGc$3SZbd~Pfr@QblA@GOPUM#X?%l#ha%Y0S0J0}ym? z*GM1W7t(*-j88}-%K*QK9Bu%;LuSL;fM0Njkbqy54{ZVWfsLpU%#wdZ;B#8NFb7mL zGK4Dtg|YlcQY32zKLUP%9R&ByG-&|LKC`t08v(x%_JM!3xF`Y7fLfOXji$kAWG@tA6YmP*Fby_9y-YqccE7 z@@NN85j+Y9D)x*5)^iUUX$2~JM}YPxrNf_q3h!Ys!ZNL47*K&3Dh4WYhX7;h+$%!- zzX~;=qD2U<%**|M6%PM+mc(!nTz{f(5L{<^c;FvPA`XB(_Ro)fmH$+T10gNtRbp3Y zwd$;RE2OG@aQr8vx^Z@#4ykON8&8H*cDRmPLMnUCjDgcz>>9?hAQk7Xu@jJrXaCr8 zNX3Uc+6SrlNk;END#5H#CrBlnF){-w-zpjDft2qijhu#*AKH(sfs~(E44K`_O>z6a3q#l>+iB|n(n*npDGviPwlurl?Su}Ihx9m1G5 z?9mpJF+9$uj;O%S-^m`Pz)n2~9!`ZFd7L!71Ll|)H1r0xzx3SDE!fT)%OO3O zL9?}}3#Q$vEP}(7a0|j$&_|j?coQ1njS2ojO`q(OsOkg+Uja22#g z{%F7#+Myu#9fRHi6`R-IgI0|NXa~T2Mzu6tR@sldRmZGZGBToGqet{!K6Z=^xp zZ};|*hw8`ocZ~R{yE*EP*sGfz2_4Z>UvZ**cwTMl?8NYZ8sp-s;VQMiT>Rs@M&2ojf}B@%)1l6>jlVD6G#GR2^l()^sofZr02h1fpj zRbWOL`)B~3gRwnZ+#uEnN&m=-hLKFYH`*IU67{HhuSO#E+Kj%8!1bzj5k~gt741JV zqOO;Bh(0{4cmK%pVUb?s38&#UJ-@T*!>M|1LAc?wdb`8k4V&t%xv^~MyYA218-_@_ z>H=n&e(CE?T1Z4-->KSTB=>obi- ze~tg<9u?t@-<9}@qKpTsZ;7@TW1F4`1;&VuD&ZaD2e=NQq48-NRUkB8#^VX%jk-lV z!CIqmN&jGr(RL|k(9ZC)?Arj_uw!n|fR16w!aaVc5pX|_t=pFmRHW~H3h9lQSuQGh z41c#Q*E}{nZJDLJFg#|NVsvmAZ+UZWYFbXrSF@ zgGF2QaUtIV(G(_3u*d;<>NbmexaWcaivU`U;Esg>uT8LZ_k^f@khZ%@(mQx=cd!&Q z@NxGp+53U`U4Q3{`?3bl(;pv~9@O z)-B9)$jJ6UbcE=et#QIV5y^IaO19{M?aBA+nVgxVC-QUqEyVZ3L9c$+< zY4wL6a99ZB8yx_>Fe9DrCm?9#yxTKKZTPm!eOU9bp>wfD+OW2B>_*D4l5?10$k2P| z3p*2sXwE*?JBCV}-5f|m3C@SyHxHe0w)Ku0+Usm_hB&0|yfyHv=&iF(*d|ejvud=1 zD8=b_!ZDGD)5nx^B6X+#Wjq!NoceQmgqcpX;vu2EQ*YHb!HiRLla`>usiI@I;D}Q; z&SP-aDS;L^SnlM_ix_lv+9rw``0wyn$(@0Fhw0Lk0nNj?vabHB!-4W&eG7-J7d&~C z!^uE}iZ@vOhFRXKkm|6`u>@H9P@9*H`pKbUuaz4<4`q1%+%_~6;rV5!*3c2pcl$<$ zws=Y$VnvIdLv9nI*Pi{})*`$o^Gv4bxhFP|CA#9-e&w5Jzh_ zweY!Te8v?a-1BNqfl$NKzqnh#^z^A37u@pnXq*=qcsh2h9qjkqgR>lr@!UjnAKdIQ z$2&d1^x%ok4+MKuOG5j9d&Eg2`|~_JWL5nJ9-HJ-eThdG77p+NJwTmy7&;677DHoa zsF3nd)M-!HxuLCaf~t6EH9TnpYzPLww#`B`3BO=oEMman`)-L!;T{f#q6D~$Te;{A z{LpcQ$O^vy%n6Y)d{qdPL-h*o-KteaTQp#$fvd`BH7XjbreawA8xQ$E6%pUPP<;!d0bZuueGAdsgFZ4LYj+zR6tNU%+!rvYZl z??iX}Kj_Fr5&olFQ$(lyndWOnyZv$dR*9DTqwM>H@BEwHP6}!M^~W(n@M7jO+QMl6 z;=pr47ys-lxxzL6X;B1$)c48Z+0X{-Bx`F-LU^jG*XMP~iBe&vz_eImbjsY_p|pRX)| z|J~10PT`;N(_C214fWfyxG38EkHUzw!m^-c!f#hjLv-OpX!?pT!qHI2b@@VW=yn6V z5EZ(9$3x+>(3N`&grOmS?5_!(LVmg~3O9#*J02yN51BbVEZ~R80yhgvL!?*E3$BHT zqaF#YLj>`)gMUNV$@oD=2=3ABU}{K9&bC3Pkc#4?0}CNJRsYA)na4xb{c-#bMWt0j zXcd(TZI)7^sHha$P$^PbQlS-X29>1}X+c_OW2hMW%-Cke?E9Vl&WvS-Vn&{a(c*WW z-#?$%yyi8={mi}h+;hI~&pGD;ts*l0{bO2KWJ=XHno(q89hX`W8HFFB21ojkQIvlo z7qUz!k0M5eHWd2^hGKPBeME)kWY?~UEZqn4=ZN$AaUwq=7G!cfFM?hbbK39fvzT5TXCJ?xo*cLEN<4jg+`OCj=u_gRWe{lO*gv_Ww9MF{ z!bLRiSmmqT)PG}n@1v>Zv6QMTYG7<@9fmR%TZ5-kien2%2*ootf!W_Bj@<+Q?Ya6(uZy*eprFP)y5bn(!3`q8A=<0W+a zq^KwrZ7AtPyalZ`>BtpVT5?jr&4aXUNj@35G*ps%ZYLF=wEF2!>h+{WuNG0AlT6=x zQ&dTlt0E~66ZLhO6xYNb`0_4kB7@Y_m6BM=Bz0LP-iIaR&xw(Umh6$}saZ)PCR*!~ zNRA1=^j)2W3B4c(l7IUv1OW{JT6il8nniz}I*b~mKTdU+GeJ*FHCotDk4^cx5>4Nm zB3o}ncSs?6SkjGBns&%(-6{2Z6KM@8H3x@j4^lA457UmMe26NgElGKEUPSFlDZ2cJ z`aUK9rZx3kN>;`Zszb`{+&qdp^K|A{(6-GLrG|CDNs)?IF)bfWuHRg?_0GGj4i zCt8quoH7|refp@Y1&uFh?@B^{E!T88pv$Uel410-x{c(!===CX|Jg7FxPPN?)pV0=Io z%Q@8BIZxKMQ;+AQZk|QmoD;wO57ja!de3KyHs|!g zEtIyL@ME=<+?Dd%6zgY{<3kGR;}>^eDM2 zXF*9W`F76i@>=r797Bwhq{<$xGa_YWYw!*v=WHg)hsesVV}=l;vtPgooxiiMBiWtz zvX81eJLhJ*>dZUtWY5)KXy22i0WKy#ZwCpAiRYlbrRe^nFcd{$5z30fdFo>_iGq7- zY9*$;dum{>pr8wMYv)o<6^J*bQnnUQx2I8T3P^jlca0RZ1xvd+3tEny?0Q^Kce=SN zq~PoMDP0Q+DlWT`B?aZFXUH!LO48qx4;4Jh=_Ai5xc$_c)K-vG5=^>Q5M7=^T3v7) z^NuJgIDlml?-uO9jS`m^IFigeNd?wS$Ii%tN${SIfAV{g=#D%2MD?2vll*e+&-UB- zY5IL__wz$Rf@0>IN#OXWz9xbM!td7$A;T{9t8wT~S4T;yk!=^IB*s$Pm0uEO$L+dY z;_rN*E3m}XUEa02WaV~)u4yI9{Mlq>$)ez^QZiXzaD>azWm z436LW(+;3hK7!r|S^eQLw2=Jey}1EQ&MR-Sm_@!;{$W`y`Dpnwr*!hhax=CA}k7lpjcu6JyH#(rt;$ z$~|(R96&ehnzq1_$hSbgNNjyCLY zY-`7T>_C!nhaa{lWlx6@R*ueVZ^5#&zN3Vv7Mw{|gpd+3$+5BRMS$ ztHCcrZ*PmkTVy|JTZEsIFKVUY#)=oUCgRj(J6kPrg35S8J+7m+lyDgLt%ZUg!@cVq z!#~9pG8W>UaSsH3xHephJQa5UcOFb4m*7rlZCWC5d&cgypm3nh*TJObgVX_mdJbql z1yp@Hc2njB4(Kr4^gy}+;a8&l};xiyQ~weeDGIJ437c&j_Jc2`;JZ06Sk&j|`9Cgd6c!~762 zi*S|sHs&G0gIS!Q!T(_vCU3_#G9RZs#3wPcvVP)MFz@E?#<7`~i=N}IGS9pt;jEd5 zD}J_MnLBG&wd`lQwuChIGgo!yHs53}rZ+U3GR^q?O{Gj@xm}YZL*EzGh-C~6bu@Z1 zgkx?E;|x$qZCfZ2fFn1NfT~_AEZGV4hBymC5ynKfAtyqlaI;|p!9zIRdNyIcaPrC| zybd1SkcDrD)mz`;i(uuh>G%X#eqa`U2P{20f}aD!r}A(@m=j}zD}w0>$+$3>bVH7_ zgj>@bTUc;QR$&Vou6r`p;sj%gf||MT>vw6*NpN1pm*zR}-I}4MPjFJp(k5>>y7Oow z4?aQ9XpDe^_;n3Ku&->i;TpWD&#eK1R}U4|XTsKFGwb~YAlFZL*$pb7gv{IbL<<4E7%0SzAj+4&aAz;tMCi={?q z@Sqv1iL}UR&ejCl4>h0C>{<7wdA(-amYK~)noYiSO=68}fV%0EX5*3mrYo9tC!aKV zYMi3&n@}2uOL>hg8v7fQ8?R`V+zo4V(%3xUG$=HdPdpp4HD)g>8eBB9-hQtaY9?1K zt4~so)x^}#QV%wl*S%ABcXZcnR14`ASe%;9--Gp1x5*yW;?;G%+}dsGPeU#>t?K8a zUu!O^LB+l0#y>vbm{0!$`h+bjM?f{YxoHIWYF_ly8=BK}eAv-oPoqG0Z(d4cxh~1> zLE{bGg|%-Q{dH$NHZ|JlPWeu17}A{x*wcX19Xk@<@IV)O(xM?ycjR1EgPHE&C7XJt z?!fiD`g^)PcYf4w)9rc?Q#Y*JkteGw(rtSgROhSP{I&o)pmX`m#b)XpYizO0bjzAg z*VgN-JIZQz>*mlzHEf*`e@;!H_LnT=8(pjIE&8@o+dX9PwN1+%z5Dg7wq@dP(}zD^ zfDu3WvlQsz-(c_2&*#>dN&|;n{WZ`p9$Z>zwo_FTTbO=ucd?#J<*t-noc9 zsy}>x6gy2Hly{-FRv++Uu=beV|LulaL;cRrmuueYy}!|Fy!9Ta{}qr9 z@Omdu6D&xIpwjBVuFNyP*8x7VgjrVsD2#30T|i;H>%jb!9ee8>0EKDBf;!{jNmwvf z~bo7(`WX0~F?N72r4% zeu^J}!b~UtbD86#0J$9Vmx|kf!h|TmJQO>7L<0E5Zv;@4jfRK~1Z^>sgJ-97*daCo{Nl6-FpCB!(ait&h44RqaZhOYABBN$0Ddv!KZb(T2@U~% zQNt$!e({zM5(Z-aGSPERo#OHyFIuZEhFULe_z{P6O6yTy#2#62)kalHBasm7N$)F) zh71~(3I7Gps#;+xgd)F#!IjMlgKq&B+Xeps7kUBs^)v?mku2?Bz9R(b?YNgAFegl2 zZLkitMt%m>ZzPqkLmixDEuVw(vb2|JQ67skWh9i#%0SsO)Vj4-WDzLGO(wFHCX`(3_7tMbDsnU#vy0(5Z$wLN>IP&?!6(tsoQOf1qWo zC&0x5;XT*@nx_a7+=b?9Vgv@zT-_D^UTB{F19v+F?o0V2vz^dX88SzWx-NY;{m;}@ z(ks*VnnBX&=}Q-+NkdHjvA-_$Heom|lrA-?c6}%LWAbp@T#3RYYS(cI!Nhg{Q%Q-* z^dqq3>NMd=NV0cY;klKPh0``)^b!wFWnBvr*H68EJ4AeY>f(Dh#lBOTA3YUMnX>EI zYZ2a$C0{uO_)%D-kJPO`dq+giNU%JZJLc#_qeN2f%@`Q6VhitfzEmS&3F=4X5; z7WU8gul^!@X8E_iRp@J(M_|AsmUd)5{L+HRV!>N1qJA~|eN-bs?U+c)l?EU~o59N8frwtpRdMNGDTboPz-h5hXdL~*qJ z#j7&0i~X@%TG3y7uX|RacKcl-o^2}L3XNx4;Sz|pYNpTk1IZ4hs6CI~Xb~!h@e3mS8#(Hj* z7&}+)%oA&z-v;o+BIae9IrvtK2;=OX z*)K|Pc6u~Rw87cpnVoRZ8CB{neB<=d} zGpk&HbsQ3Y5gc^3l53@_!e95;=}aH5<*8zpnw3R-t*^Om*HOv#;1 z928OFvB`aQq-4#eW!7Pm1)I#5bxDjj8Lep+_qmU`oDjFV4{aS0zjN=~86?hdM*?1o zPrC~aE5)Afl<+xX3-_k8n?wWd6&H?)n%rMpJuk|1&$)G16ySb4^NVPX`=y5>A|JnSjcBiT`qix>EAPv<{Dp}3xy)qY8}E>Z<-$|mdkWiyR^HoQ^}tN; z^&cj|cf9SYm%`587WEzii8qQ6D9G{>kWUIWd3|D?;j6rE2$T4Cyml*+`PN<*nkrt^ zwjSLmZ~C_8@tthE*OZ9~2^??UWR1x%h^&L@G(D26N zqNpJC);S{IAf<1%$UaEAuU|ANNN{+YP#Q!(aa;H;sN>8xVR}$QyhwN`2y;a%TpCn* z3k9o#@-jEVpMugJo`$1?E*GZ5wn5RaJ`3QWlkaJQdqGF5z6)G~_SH}0tAacTw*1FI zrsVbfErETk%{)~gUg*cW75H3nkY^Q`sJX|*1n$u>xw8XTjxS~n1nvSZY{T~e7c)-K zpedr-UoYX) zupRq82ou9R4^I;A4RbrOTeu`_?U`HfNZ5+_k8pd~!Yl1?ZkXv!7(N&_Iddj#89MsV zQy>gg7e)vkhl*d_7wigUzyHkFhjvxb_~oI5y6^m;&}xDa9}3MPTk<}JMzEIf_JwX1 zuH*KHS}3-1?}m(M61bKjEL{WVMo9Iz2`f4ToD0$VvvLR$Ejb$nO%@KFSpYQ)znorY z+%9|*sWHDTd>YAK@>zH{vT0ScFf#Jfh7{qJ$X8nq39Tca`wYRqBA@O%2@4|g4zX7ZS@1RTc!o-FC362mbAfB5ci|TPaOB2U zr}-Zu9o}c~!y=bdec(@xoL|?)tBEwh_wr6gs7ZR>w1`%gA-5vph0vV4D3TzlFKh6|zV?}3X3SP&u z<2?lzW64)e2s~o(H*W|GV!vg4<9EcC=l1i{W1kn!<$K3wz1qV29eeYAI1dw>SapYY zEEeo|%$pJ$j&I|Bi`_;NaKmC3FjZVAW)vRdJda^2rg5BOK54w!tugm?57;YW;`LJI z?wELx$=P@9ESLoza1A^);GL~YJ;>WsF?*7Cgd@<)& z;w{ow&YQ#&OajLzaWl+k%M;Czes*HQpvIOxIiX8;ffbifp>JnYCX|9S%k|r!FE2>G z?F_ai0n@*wX^ivP_SyelbQ8GgJ?DXzKc z-0_rEPqEzEltm?K?u8W7_a@vGAh6=Z5hst;9pq#tYw$^&4asy;Hd~U6VZLN1C+EUn z*k;Kw2$}UE*;ix0a!+2W+s}M^V?zI)G4lqf{PT0~qd=PF;k_!Lg_&RlU;g`ybEsN= zdAiT+h5Tpf<_pUB8R@7M&-u~l@9Tp2yU@JNY5e8r&K===V|3eI1D*zn*9asjo-%(L)(+iuqUCXGOx1Uq-o%Xtj%d86zKLVp7UehBR$b`oQ04{+?$$2wlfLEWBXtS61Hs|E> z8eT@uzO^5DQ8{j#^gN%O72Ch_>~favdBH>FEIGJ>E6A}v_L^IsV}5#!dnsr3`K?^H zoN1S1IpaB#QlD~Kvj3zDI5)FLa;-R9v-_V0vd6P!CHL50v)ScU?3nBhjDWo`yS@&^ zqGW%>+p!X}ACkOSR@sTnU}k;xVfZ}rK(;HA$>`3WrzS8Gz&^C5^u8>*K9*LNRR*#L zUC#?af`a@Uj0L$}&q|;vTz27UlsQ-S)M%QTOD|wsb#QA62=+Mc(*lh1C^xC#`KBmt zaKZiU7r7e>(0iP?a|-SR6FGea$;bRT*n%sk%Q<%nF2;>;b``{4w&zSOh)O-grWTw? zFJxyI9L|xmcNO?QozFHb@GJ>rH5IHczrji@SdMwivM;d4GMSu$8MuF$H}d}>nK2jT zOPCG}Tz)&ei*Y!=963$bOniXHiK4lP!<8U~}x#Gh_Ib&wA7ei04EOvyg>AQ+85l@<^ z=(qYB?QD@;+eIBHYSu5OIv0U@1AF9C5Xc^AKYQ*oMbwcjTe+KkTOj%%y?J^m;GV*lu?;mU_b)3ZzNZb9-D9-Va?7p?`e_bjQOH~>u57=0KXq@}M(rC4qimku zfMWUX2gn|bea!=-+mWxwfNH*g>Hz4ZP>9jADLEIwQD0|1S|0JU~sW)KR-q_ zmL6ozNXL>xsfG9Z4=sWZ^*tgjtS~s?!U=i&J zHof=+%?q1UmQEeOMpS;LKEdvqkzi?Hc<+`dM2JvIm3hpq9YsX#@Rp2Bi(uei)^#PoNo$IKpuhk+Btj zX?8zj34Y(=LWUvUbM+Ux6z}XZM*oUmyzK=&4{yC2M~}x_1iqyE;7vor>E`$u5h|Jz zZyXy)`-Dd&7Sk@^^vV6SwYZWt|Obx~{iWgEL9Inil@&Q*_d7cu0 zdr|wUYZ!O0h1~TNce!(JqJogqdkeBat!}PS8G(uCJ=3rIBJ> z*3piVLOn-mE~LY|?P+sJL4k9ry`+61Jya}dcSHj9E@?;1cd9>WYvNYwERt*TBMOtW zHcd>)BCX7Jr+AZW@-w@JNi&K#UB$$|?Cvtz{3u1a-7I6*n%20o2Gx5w=NM{_;A7l@nb3ty9mcxw(8p8oq zADSa)C1gw$vZp~~)N4%TG<#|&vuVK!st2>mA&+X#EZum5GQfQ7xrahzX774TdBwbc z08L3^qC-woe3-XR{ie)j-iS%_{?Y?YUr_ItDp)jSJ@) ze(B=DJyyD|T3EI+tm_fX+;FWc3MOoQ(&Ys=?bLV8hwJzMBL9GEk0RtI_{*tVW)P8X?}T@6;fz zSt~o=Ak9x6b{<6PUXFGeAeeW~9Uqa>ic=kXk-VBW?E^?!v#>oAxz=gcz63c>-_cfs zMDVY+`5=d74Xs?nzxQ|RDa3Q=7-1M$I|>u35p%%1AO8R~UGnYk3ZMtRgGw`Ld;sKx zNjLj7&|IQM!!XDo-qUzl#1JDiPWBw)X3ffVImB6-1s;Dpdo>om?>bvFW&ym;JdNoQ zxbu`|*2%O^N6qwTi;jOZ#+TANuo{Ew`i>;^pSuAa>(pZpI@<@-Kc8%9FH#S^eBHiB z-TzkE_ODt|Vb%6b&8a!r=Bn;!e%8uWH+1k@W7HV>RKj2NTRtGZ>Oxr>!AzaqOT-te zZx1=(9n=Y zBON8WV7s`EE4qNS4?FyIz8(%8i*;T;y7p1s)_~3J6rIPB{p~Mwn@&z?KcjO!_om%R zxBk+sHm%O-dS+Xt&f$)tEm~*);6$6PZfPE;m8)C$a(C-p-Tb%Nt!s62KT`>O-Lx7j zLW1^h^AW;K?az*<_;*_H6v4Y{W&BAvf|euOi}TTvdLOnBwatV7w(QeZjoxS;(-wod z_S^p~0mtn82L%}M@82JRmi(R#)Z&*bG^_p6_(s%t`!s!$DXC4VKV~azYt$cgzSWkk zKe*YqEkYmQv$k!Me(%1#w&{BR!%eMX{qFEvt>yZi=T@|y*YCJk*t%Blb$upbP`~BQ zS;9xX`~5+}DgDO0vxIs2wJ-YcZTdBD*WfScSAM>Lx76EyBjW1x*3Hv#fqK)9fEJB@ z3N5|m`q%^y*J3dCk8DPB+Sowv-e$uw#b8a-%`yJymL~Ht+Qd-nhyR#M>+An(&j0o7 z|J4boNh_$5pwMlG|ILMIn+hllv9-u6b9Rr1}KbWV?UrUPaAJeOiY~V z2mOy(e*NHno|D%P7>c>Le-t&&h4JW1g`i{e`<6n`>B+rd3?D1(1$S5Cr{3*=U&Qt(0EJoEQvyL*lY0PZ%WLRX z0e*3*I|{7bX4VZxm>>I8cLBdhSD6ETaX^^~_=Q9XzMq_-0OL2-DupQ+QO`w^LFFHd z7y^Dlk%QOTY8kkD$7E8FHkg>`_XQLtvLCcfv#a{8fQwZc@Lv{~Xr2QKBh~Bz6sBE0 z04NMu4S0u#LhS}9jJ{6`D9rah@R{I|zQcgRh&mPb- zyZg5%9Z(pg2Yi3tvu-AEaiseopfD3EJ#Yc5t^yY+su{q=9wl%@kSf6^$ytgl;9{c! z)GmdK5I`{yA_DGMwMc#kxag39Uu#W;bMPX0EY@jI6WO za>&T`ruqdmc@;(-08L(BtA?P-o5%X9pvgOC^(8`+_AKpN0Zj_F>m7h5g{|y;2^mJ3 z^@c-+u~T|yLIw%)9v+0c(b97hLfyUF;{~A}TivFh=snU@ir=zgOl0F_ZIIFm?24Y!e^yNb2#j?IgsC8w2pEK0Bwz_u|`nt)zw+Z^_ zeWW)VdgJ%1_aO8v@O7^ploKN8QA2l5k$P&O3(?A+L@4M|XU{fh?R9du9$I~;sG9&e z-2d8r9$NYMakmq+{CT>H1=+nmqPhdwemttOh8BNWrNl$l4NH{!p?L&_ViYnXe^sPH zb6IG`JZP@)4gz-4QtU*QK&G10^3Tv5-37TZG)Mnl3icfbF6PfQgUtHQ&EAa4>zz0C z!&I}Lo+(vxCwc@^5*B3k5U04<-{`3_{^MlT^Vqn?wWufF_|mp%J%Pp>c7^n;H&X1+ z>zQNp;0V8az{nx|S9jaw$LDOjpG^9A(Y^b$Vg9v%Zb!p&w-2iHh9UP-RZWKekDjP* z8@fMxquOk^ytG0&VmRkRr}B-#NHt9vWI(Q$DSjC|AqW*84DOKGid_bmSsloL!8suf zxor@tXhbXw_G^0O?+tv0=gKD;c#R*H2n@je-8ZrrJTLk@7REwNJxx|s#y&k6R;x`1 zdy=iD*^qk9&mXoE^n}e9IzI35p5NkX+p~E7N6(U;$@8D=RClZA-`Kyhn>0W2NL=^Z z`F>!kbLxECv!w1o%byq2-AgT-ul`kOEnnWAscN)LxaXp}YZ>t9uxhuZ!?QE0X_khi zsmcxu`Gr>)Bwx zz%IFGsr_`v{XJ9cCR}>Dd+q-5jOp&O)9kG4uCNmajCN<)v5vTRpS9}@kM8!et38|F zZEpAWLY=D5F7GN+_08_qEtM+OF7nZyndEWifd79^%?pw~NooU?>PNM~*Kcp*9xv>k?$|Y{irA7*g+vg9qipOpx)ms$4Zn^b`kTJI_gmVbS&6j)y ziFBLENj>8-nW+dbenr$Tysb@?tRh)Qt`cmw-&1Ez4z>dR4=@J z0)kZ6y*&??s1A9%o?xq-yjPw1t(xq;;DU`3_MU##QCaTw`<93Dg4a;ydF4hg<-_NS zF)vObR)O_ue$}DnGwJz68~3zp(%-)pWn%!!gP} zzkw5Pm3Tko3|m>?$BF+(dCsr%>J+7`ANH1o66Nwbx5^*v{>k!{Z{FP{JS^+pU80DTp?4>0 z3S~=o@6;jEd%Ks6uN5EPePCju`z%x|cn361MLvi{1*#%~Cry8*3J7Yol&QRf zDr_xOPC>=16IFAA9&Pkj4h5xenNTu=Zu=%HtAmpFNtBO*E+1Z}j0?JO;*!!QDC*2x zrA<(1e3xP{DBy}h(Hyk>=CC3oXk(_mVqehmhldoVL6(Kr5LS@Mt0Lq<5bAvkvOQ2= zC6kW^{;2yc{}3o9n9Gj^R+E>@X9c2J&a#%k6GCrURG^z;zsxw$RFfusd!S!Ol}jPVg;OAnpQvXHG0kqRB8qL6xpyEHkZKyzMd8FEQiBS{Mh95)tag&dohP!^pn zf*@t~*`?5AWkeJQs#9)`lo%H(og*Kb$180kuP=F{oEsUj>X>3Ya^Hrt3V!5{E!!2f zkz0Laio(dv`+^i#B3%yED1su_o`4h%k*m+RBNLH!@lgmn(&kDU@;Y+P%_1Z=a#98t zSr;+-aH@PXLR080$3}==9g!zS(BI#Xua4-bDwfG2TIz7J+=wc?MCKilMH-S0M?^8l zrNt53gtMgEBjzg>OS(^uY7R+KPw{od;{H=uy;d~kRQ1G!f_nk|;4( zt0;)uX24YB#C|txQrwN@E$&bx#5Oy$C_-b)*AFP%VhcTl3d`8XK559W*qnWn5K-*? zL-9yWZ2EBmazFNV)L$elHaXr2ag0s45+MH@d+uheoE#gLQ6_&FyFXVV4~pGhXe6H- zyYbap89R3Q`(RmCtWDJwnP=?0x);)+*s1t>>FXFZi7pL{X=jQhe`88ut)w*On!-@B zB_>GYA?C-d*P+F}G4u4WFe7F<$mEc(fl3-ex=IB~x$+g7jR=yM1~!Ny$z$$GM3`i= zh>wtyCOMcPUlYHtk3=3OiagFD7ZTY%%aFiC`rayJU1HavWyp-g&f}?aWg;PpApers zbpBuYt;CuumhydxpKrR!=O>nCB*>7&C%F}}H;I`~#j=RR>#wHBmL|r%UnP|#hF1kh z^AZo$B}#WB`s1HS{!Ls>`Y3sqXu_yU5q1%!#@w`;%gFzFtO5nS5C$dzkz; zbwU=BJd)udTav8Fy(aBRmOlL?eV5EEkw_zw+uuXdMakHzWfHK%OP#;ue)3Cvti(O} zHtDXoFFAslCw`dh4Sy1^OmB z1E8*%Z=o4-zl_zWukuLr#B39JD4Mq5xqJ^AyCO^Of_}BmLvDf2+#D|(Ltoz!AQPf5 z?-`NRp%V^Xl|4bnAM2H!Lq|us$lTG9=P%0)(Z?^pl6IjFr*=y7&;c2fq({*{xjUpb z=uJ*1CQr{9LV8EU^F8;_fQ9!$g*=^pf1Rs z<(!@rLXK?3k!8`!wE0RFLgQaua!HuVL;Hs!!mivoiEie03IRqomiKUjzw?8_(AOO?!3;nj(E(=!`OzwiI+uBTB0a9$D2$UliQ5 z$4KuM#5l{Pkp+h~?U(u#_-sEeT~Xk*XOVPD!IogGL{;E^%uRwV*l_xZB)z~XP9fP} zu=4Ue$-DyFR9~^Az$X2kxU|45hb=x;F#YLl@uC7$$sUm;|KIW?(bN2XOtENRz6?td zLHSIaPFRtTCCw5Z%74kUgU9o4!`tDf`4PxT*fHNn{anzRzf3zI@XAN&ckQ9c(GcFA^i01#60M>Lfm{s8rj@_bIxiU&gyu1lIWw3qGv|*#q839Z>eifdBM^ z6J#UimA^IU5ORB=GrAMgF*wPj=e8^v?VMuJqL z?`7JsWD%ikAhK7KTh9Y2MLZMArZ80Ba zmX($LfiugVV^+W(Wx3c8!H=>#IJDqdSuF9rz`HD%(Z=sB+al=Yqsvwzv-k_k%+z~% z)$hi%uXt|ngkuxjOYdqx_CWC!v{54YSJ2*yBwq@FimLBJrXpe0G6R1R4)bE3v*<16 zj%|P_19Q>wiYN+m!Yxd+9kb61FS5hz@~an3#`pv!31t|sFdN|)j7Ma)@HWOJ_J`0P zHhd5>uOJkjjxjENDQKzux2#nVU)f!$7A&cRu{L~W zB^9@upInJ0#`Ene!P*VHuF403FTAsr*X44aVdXjXBJQ)wVC@y|(n^;xAt$jCJTHVJ zjo_&#)HXf@+S>?f3_`eJ4m4ZXSw}UPA$)`VId{D<1It?SOBjvCugMnf#8$bM3YTI_ zyp{_mVW0WA!houPXkQEVQD`mv2%8=042NSgVhiC_*xQM6few2kWx1dRn}og~xQdO> z?h?3Q&lJq(4`7cJALkcgca^2{{jnP>Kl8@0wzU%8JFGcw8ZQ8ANLErPg>!r;AfDQ?VihzA}u-am+wik31RR}NtO{|JO#-#hR^#bA#hkBXkh%RG?nFd4~kUjXw1=ThFE3PNd zbWRu0qinD~3g3n$gGP8PX3aDsUL7-M!Cc-;<{gKtyz9(_jXQb!nUS7uyfw`5UAK7C zn8yxW;3}C%Ljt%p%-~bK+`G(xm}u@k=I(?J?mXu9)pfI*;_Gruuv_~7pupX3${ z4x_m5DB~RC;ZPm@FXQ&uYI+6ZG^l`{m*jxdL4*X{^_B~0fF6ubkASOeuIcsOk>|dSnDsc z4Itoh6Uq~OUdrV;Y!rJ8|7GT$v>L?SmTf*B@AY)t{Z5;@jPrSC@$bWWuWBRlC^ zXb+Li{7W=D#7XvzT8r5Bj#76c7DGoVGGyu~m-12$)?Q+N`z`~dz6>lR2(eSY1M1Co z9jFDBMRq?CROZ4ol?G|7Wf~icgDfM>H2Zp{N@KY08na$K=JA91SgrHTXU3|B0$Q2g z>K{ism}ct!lNT9Ebzig*e))uM zp>BW6p>?PmDyGw7)fF|tv{~ws=4@)U`f&$^x?i11H>7CPDSUTIvihRzCdEX3y0^9K zh5G1_U6-@kfAk$$t=n-46pF#tog% zyseB7-CDaq#zx)hwbvOYI$MwVbfs>AkAmK)vktJQKhn)VvY8&CGY=o3uhY#v_lWje zH~ZoQji8%;{VMH_&iD?C<_V?;1yFx!|Kzn(zi7u^Zl<2rj=a4|ou?i8+(N-=dunD- zPHKhC{uBc(ts}duRNF@D=yKE6@&1y@+6vh&@*(Z(-UlQ^TQI02UC`bey-Zr71^b^c z9{;8SjEIJ<@bNgg#t(iP;i9-mH%Jc zftpY{0fot=g1+ypnN;wZB_t~7dpX%szx+pGsF{Gm_);SPg{h%}Z0?~ksu7?tUnpQa ze>R0u1Skwbi3Swr5@q>+6s8O8GIf7n*IPhgI=TRjdvUmH4xli_Wbj`;^^ubRh1o&2 z1r+8Xi3TW)jC2W57z+}pCi8-bAYmYVL39CC8)hfya}2KOv;!38b4LrHFzDf5U|^Ip z+y^KOeYhQhEG7+u^ReK=@J2vk5_EDvVK(b-0t(};TLLJ|4s8dZFvql@KEHRn))r8h z;32@sk8K)y3nfyqO6raz>_sk_2W9=7ll7S zJMw1h_ZqN2)2#1+p?s+r;DgHl=w2K5>> zQ5^{QMP=V>z%QtvVh<|+!(Rb~sTnQ>6lQD~9Ig5B;hn&RlWq*SFwm6&3Zu~Z0}9it z1=pF!sP-9fF-yA-xIk&a`0$|V5C>40zeC`TiX0m91TKh!fGi}w9Q+7eoE!`VF6;(P zfs08$>Vb>aAK>})BK3z6aIxb1d%!RLe)j+_N(bn`Mf|`4-~tRafs3~O^T36Z2K-;4 zx#kRTK~sbG)f_e89a=&k_}ei>ug!lhbOX>#6Ov8}nasScYk~~S&g#I-N$WP9AB3{4 z(HTLgRYGk8gj$biV<42rd@VR5JDi6AG1;?os0y&8{X^i_4htM|04}@+zXKPJgKvNf zpNCz!n$sKBi?APjsvLL%n=Z6BJW#0FPY$01e#^4~dG>|*k z1T79V8oUcFIyE+U0J4dhK4=Ebzclzm0?oVr{l`nl><<1%IAnI8{lf|}dtCip0GU01 z{XG+!`})#%cWCa%3j+u=_siA+Fwdo7-M|9Kl%VTxgiOht{xE0`>wg@bcT^Ki8^xDc zuu~N~T~V5iq9R2_0Sir0L5f5~34#hXs!9=+rV>E~1eIbTfM@^<0b9TZC?(V+K$1=G zJtW_}-#^dsa6B+~v)O&-{_f12rn4Zup|kW$kRI6;RzteeI3C%v5l32@%;b$T))t>Kb4y zMfmjA(3=rEhFa)Bhz;Z#n1wK>|AVh1jQ9!~0imbZN-07d1S*imyPz4|539nVcbxr8 zmD+PSOP5CL4{;VP^U12o2cZ{8B^v}zQ%TP$tuJlU0&s~iW}nU)LOg4r<^n!PXB2P}lXbhe!ZeAJe`z+x;SoJF(v zbvlRDX7T>qOV&M$msdWsPFR#&>u1?mq}+m-VvEqb7R*l;ZV#QA*DXvd{h1yXGhdux zsLYv->5NA6p4NMeQ|8Y*o-$T}^D64-|I9?a_4MmzRYO?1mDy2pGu&mSPp831(?5c> zv`?lO#W{+r8CZkJ{RLLRKwQ^NqtH?|VS_Qknq6-HR!@(8&;Hqpes-39q2)^UMf-*^L!SnF)5^A1-1#*wt08XNc{JU+iMk+hsL|Frw_vwVq=v zv-9h?LH}!K&{aUsvHjh9pT6BTd*~@lvGoFXRF174eF!$QAqtk$(rkVxPEzLBW&sru zI~*X0Lw50o7O@|1{iF4TePFA(t_Ry~>)aK)*&Ce|7Ff24Gu8GvTif}cqZ&2N4i*X&Q;+umY4IR)2mp<&iUtDnIh-ZD}Kzc&gZU0GxMF1 zIVH>>=fJ`a=1OOel2OK(vvq}nQR-~;VhJP2S-Wu~V}X;T)ra2hG}aMHzwA`k6-&2u zy4ZUGCOd5%%7F77)#QA5vtuXyE3L<|STKiX@0g@GH1*3-4c>uGd%!cEbI9{7w19nl zmy6aj7I&wUju&fmr`~cjt9vKHVh8J^hro6p>zN1HLCDJV=yAQtI_=T9hsWCQ(c!zB zW#jSw$UW8^kGJ99nG+tbqS?&%9_8oeGxI(2udHH*d!$~oVXpT$pA*L5dz>u1!@znR zE_uPY;<3Bp2gAW*b1h8gc~~^gqE~w?ZZ)MJbJuh@({8;Q@Q{&s6txJ{jNQvel)2PYti-F0v)G@os-KU*t}b$g%oB3)L3 z&!1)OtSFy0^Ey_bPlFAawaur}p^UZM=bozzbK2+5o(3k#C(Cy}v%@F-NEoxs=W2K{ z^RmzR=r2rPpHuNuOmiQQzGsL4Zkf%%`E1YG&baAgUl_wU;A2v9hq1^dkDO1GBsbJ51n8?^j(L;UMoDz1!e9-oZorY45!4$sx25?-}&_RHaux zf0P>Gg;Cf{t?(KLJEU$s@(8HV4pKq$SUHCywR~B6fzKCYF{c9~mUc6Ff&0woGsgp6 zY)&)31=>06U{(d1?qD%*2CmqHVx9>!I{1g_6R3B@lDRf;es~m9D{w|MnlTihh=0$h z3*cPtVWb31q)#xs1Nw4|73J_fk@7NMU z3}Z*^HIokv``GC9O^ju+ew)YW%2-#I@AQ#aXU_zBTkMttB6@l3hM)*~a;!~Q6a7%^ z+EZNm+E}xAV^|Tp;<7vZBUUdx6n+pp^JXy|ijo!lflX1&VmWOBHCACst3Y+vdeK5q zUm8#}UDW%Q0%`}Uz8ynNKoxX;rCOp;J-w8Fs67N4B^hNxQc+gLDCv$M83I3JKq*dJOsAqFgev?^(XjW>il&FU|fn|6CQq=GQ52RzLC!Gt@a~k(!(U<3l&@jyXo?ivsd(D2l=pG9w1+8gFa4$=Q|i)qG~1NtH|?n6 zl!pZ<>id-3;&N(IO8S!?s#8irjff&jIn}U?QkQb9#hnt7vcEl&Vvw?-GimBeieAs{ zsdLGE!n3KR$$v;6CtH(W!jj2D$=Uqf6ZmALyo_v>3_1|n_^n2eW})0V4yq#O0<;ib zmCe^`gx6)g(^104SvOYQf%URbR;jeCV(NxWNr59pl*ufPr8H#{ zpFE|U&-`1{NwLlRUeB4LX12B%O_gQ7Xx}#Fo0;EvXi}4TuIJR`t4x2wwaLKD4W!2t zqRa(w|3pCsjc++IE90{~jg*n`45*lWmkq|h=x!;fYYRJ|`85B62(36;SpJHI?KJ;9 z`bvA6M_%9B5}Iw^$Bj2=x_QsHbyEd-MZ56SfxLqK$<%jwxrgUd@8;bK&7{WUWkn5A zUGmc7R#E5XUA?rQ(wmo%c9rrlFE+cC5|M|@*HFy!jumg6n#eo&pxE|$y?XVp8S=!qFsM7Ef4Cno3zWF?AbLjocoOsHgP?-nv^_YlA8s;Bfrl@@@JEs za^2(+#F1R!4N{jq+7EIdCXc|pjq32w8=6B6FV<)+qGmtHn)ig7^x)v~r_`tiE^8{O zeh=1c_(k3FKxf-_>SDBd*M5o|E#I$88A6K>)l%M}1)=LH`Djj5CM6tAKif;OK~G+q zIVD1mr8!M~Ko4eLoytJ}$^SC72mQTx=F}|o$0zQSU(k&;F_Y=&s`~qrThR}jJ0^JO zTpT>{6n&v{_Jkk$NY5&=61|wV? zK#~-fr=Sm^?615HAt(tIhan`Tyqr19gmS;UX=x=Twfw1N5d~RZutA&RS)R1@5yi4R zX6JXx-14Y>rBjUZ6Ng-<@a3V$e@;CqKNe*@b-w)Y*|aIwa^H*1Q?tr_QhAfV%XepQ znM9Ynb8<|9YE}VeM)VNl1QyC3EOpYx3 z(=8w`1jip3kebRKksL{ZWohs^BB$&GuY-8G%tLNGUQ}iT5)|PtfqyU+`LZ2U)XNp1 zX4Fcd1hXluh-=o!h2#tDESXU^Qg!uN>&d;<6&KG;>Qp~Yt(_RCMrUy*%Bu78Jtj_6XFs?- zVO*V7-a{U$K3}aQKd6qXUqkk-4sJe1o?U$amq}`^cEs0^PFJt){!B8co;5f{Y^h?B z77&kB^~2udqN-Nj&E>#8dtL6Ox9UC3cTJwH-)x7R++T0%ylZlOy`IO*$wl?^eQG9n_1b|aCw|w@I6iNp z2BSH7Wg-b9j~$%w!iX*|pU}Z@Q^U!F7xePOrrzS^YMjq@Vn__y)(WDW~m+E%X zJxmh@Ci!4$npct_%wwE4@eL*$A5T1iiR;cKF2Dp0HjX!9+=!#&0T>&2#TXZ}gqJXu zUMH9JkN&9Z0||=U4-Y_s;?4))luX=yKltCjTS<`aL{Uo#V!_0{=Jy8H6JXVoDQ_aY zIomdAVn_2ur<{r9%~2kDWOZ}6k2#st91_4Iw>2LLc}^~F4mi1jjA}j*dxN~Kd2eDD zd1mv@R0GoQW|yom(*0(KyjIeYX1fRTNK2Y6%a0Pfo0nE+5;L2%F|UYDSTS~VoPmXL z^Tr=z`|+E{y|K9Nz%e1VcJS(03HCm*c5FNL1`Qq^#-8Wx8jZz<$X<{1VqHLjBKHe$ zV#s&C0N;Xq^9yhz$XTDaKnuxP?J}(|ybia|qaZ@H>Usc>cn`>lq+!x2MBsH$d zJ)T6yVZ2L7UvV!2E|H$$szRJdDYyz`3&|H(8f!&bjeC%Ikx0iCrhFwf;cjQ?5R-8k zdGSP7Tq^q8xC)n8zI40^7hUZ+9*sMKi5p*v^Tbw+eaCHT`!RMAXZ&3}W{R8Fy>#?P zn|RQDG_Gxecwtnx?Juoq1k?6~r#s@=)+kFG#<$%E*@Nr9mI99?BkN*B~WD|YhA9)J5GZK7Fk6T~ATda~xiTS$FV)`4z}ZWdp5e6j-TtX^@<%o*t6b0eB8QcZLrJuj2^Sd-m(6kRj5N_ z&wG|8G>paf=%*|ibMBd+d3cu#lEz~InGcM{QHD4{!+c6exEcR2Sgp}N~wwvOQ6 zy#-_sBEY^c(D>2+_97eK`QPp}<10r@AgwX_upVSL)CE4EGQ$t0IQ46O8H#hV2O2?|u&r4oW_0hOQ6N zzi%6I8XWn9A}9ub43rWI2S1IE6Xp-Tqgf7C48GuI54sFKlBx$JgExVLYc>UZ&9P-u zz=<5wngpKT=-{L~sE;RHp(Uff=ujyaDwlk!gtjm#t6ia9$xNXoh}IQ)c^p1fx`ijtKn|p5goO)S8;%M0jjd)bIhg*JX6r3jVq0=I{)-)A#Mr2>ktM(@;J9 z^+edvHMrx9aOeR1@d9$l0B%kq5D4&_412;O`1PF{!cn-U$dF(FSC(cD{(v7p|2mig z-+MhhxE9WOw`brFeCbpAz(qLb8*X4Z9R6F@kAn{lZ0-+%y~dOJIIs)#Q{Q!X1J|r? zIcz3<`mYxTr>PI$5)A`VpC)<_YN#k2R5KwC_`81uL%>-)&Hs+rHMEp(ve=fO;?K8q zC5-a5H%1Yfcxsn9gglW~ z{q0)cbzbT3kA0@RyZ!Qit-S2<-T(IUlBwB$M|tra_TLLUr1ZjHOWq;CyD6$6z_^E0 zKn0;i1>PEjROK&FtrQ}#V*QI;q$M8AmT%U7GI&ib;OF{%{<6u}L49B~#oPLSuVe!s#sBs&d zOzuD(pfDQ-z5xpJZvfB`d$WNXfWo*8fSqA??->AOj4zTr|;H(JCgNz{x{kV-3&Uv(H<=DZajJbuaR_g;{k=KR)Stor$&ha z6o#hU11QX5r7qwXEs8IIU))fD=a;Ro0?-EsfB8p1VfM>`pYM4}ZUp#+uk1767ZI{- zKw;cvTL6XGB2_`qS#9Y{;QWtB0TD=fBwY>o#R&-v_=TGUB+2e;B%p_Wj1`Xqei0@v z1Qce27`(CG{QmEreqSWo0QiN85D%PxvJi}?H%tJYn?rL1z=tJO@c{#Z+xcK#$E)Mz z0Dke83r4F^D_sGF8BwkWDpo5&cfKS^0p8`SJQcTr3M0iB@Nm2Wpn}4k^PdXE9cbYKE5%i4?xJ6cAV|ybslp0^R`BFt?qohHe*siL@(7>; zAqV4h*&qWm+ueI)_W*@CBJ%+h=7h`;P#C22H&Ag{3Z8VayQG0Y#R{njpfDmyAD}Sp zl1e~f-buWHigd|BpyHqy{N`t~#NmL#Fhl~N;-TmfP;o)D5~$DPbZ z)P~jy0)UDEKDZjNobLrx2zV8M!mQ-*{!<~3)xymBE)Ue&F*hHK(?PdPCWaO-vjw9y zG)e!Lv9(D!l?3gjh-4Ap_(}=@Q5Q zWhWVd^b;0IFpz%oTuCgXe`8#-71F=W6|*4yd%whQA^k@m#G#Ph)7#?ZklxFiqK}Z? z8$S^W(tGbAoQCu|RKjPFE>PhF>GtLbMnOU-KyVz=Aur?8AsxCGKMGpJ_v3+lzx)R0 zE2Ill*z0hh5?R3l53PP_^o%ESYoyMIyM{NU>k!$ixY8Af8*5b3If!)oc!>a! zkGSOSCi#F!@M@KmASn1TBaKLq&pvT557SpQXj6H}kJ(k@zFLeyt(J3eHW0F;FpU^+9N+?D$GQ zs8I?T{g_RbbQ{^~|CW3=(pmXQ@?iPWqA?OY+<@B&a2q_6|sV zmqhy+Nj5DBIeJ2(zr-h^K+IjT>GTINe#z={ed4kubFRSRxW%+<67i13pKh6o=Ptf? z*Im@N_{_sV(WAw#m8V5vi}hbz7p*p=HQpBv8RA;2gaw9a9Sy=gh6}r}0-3?%-g-f` zfz?o>V84DHxq&ax-$*C(3-uZWraYG3RRxN3*Z@2`Wq;OT!M9|Sl|K|E`D3A(Ss^L2 z$k7u>iY(5qbeH5<_*tSPS1q>N?U0iw$x?Hf?~qtw&OT}^ z9x^9I_=~aTe@>@}3(Y^Cdn`U>{`yLz*vb6SwXfnC=4m-x(I4}$yN05N=57zIMB(Pf zl{-b|=2|a8gcP&M#`D4ov(DB`;Ssaa4zy6$%(JUP@ZFTwTPe6?T0HbZU}Cz5{EGk0 zL`5IvN0~GVjCr3;3KVBJwx+{CMdIdj5F}-9@_~#bkL{BY#*!2E+PZQ{kezVF7m2qW z$x>Ic)vn8yDY3L`c1)Gbw|n6>N6fdY*c&MxuzTQJ3$}BGW># zKD*QBPKr(KLawBUgmwq6-4)^Nw&%2qZrNEC(nJAvdLk*&OPg>cw*;)R3ovF)$M z1HvHNcdba_0^0{2iGuI8N4sta(rgWSvjv-Nehi`cB%530Qhtt&Km8Bi%4U_ofR}AE zM-j@=viSv6%yel7D(-IYffh+lZ}rx~NsOFxb=OMfI>)crELJ*4SiBN5o%h>577sW( zINFKdJFj(Z7gsv3-fJPwc3$jzUL5Vb@W^}dZs%Fy46(73>a?y%=EOf|FY0!hyy7OR zbQ-u8B#LwD%(*FYc4{qrBZ8c2z%egho$gn#gxOB%FXjk6onjl!1Zt;at=k1}o!mQo z1#wO~U15SXj)dMQ{-|T=&?SDJV;DJu@8r0S-pU(skO{PS+a1Of2iad8!P`=r;yDad zn0X?g1rmpyU0N05HjgHq4dPcGx0h#&%RQ1UHjDE-Vr)Icmpy_UCd9!WyIm8-?jGCr z5XF`rn|-&4wLNT(+z^3+aL#%x`yJ)$75( z?8X`j;9I%1kRy5hZdvqFUXoiNALf$WY!utrSKSi8JDhwFyn7|32Zo{fV%q+dTFK%N zpQ42#@ot}E%NoSaKHlcf#A|)F*$~8fKI&nBd)7cvQH}`&CJjVA{L1 zqD=77JHPg$;Hr0e!;oO3_k|WIpXMFfq04{by|K%TALKo^*P1`yi#)W0_sQ!m`5^C% zR}MXgr{fjE@8@3ea!{;guk|_!R4hCK-Y4R*phjq(IQsBHt^Fcepz8vZ=x=~*X}joq zz@(W*^e&*!CQMWj@Xf(ilo5d4F)TV0fZ20Qv_IhG!8VayK-Cc=(Sm>{;YWmUKuL6t zup^)_{-v-u;O6Bv;hBJ>^lqV304ise5DEw_bQItNd`m(Ew*x#Xt_i#Yw$xS#W(Qa` zwDH>lR<(@r69VS9%lOv*KDHtty@{1ak#Fj?3TvOZ{7SP)_nZX%2cSrvU)xHDu)e6r9eWYOh& z0(!{I^cn#+SbTF_a4ncts4v(XJXEq(FgN%|#R+~_a9izles*wELj~V8_(e+xPZ<2L zeT?@qIH8lziwO4VQStPH&4%>2pO2}@7Tow_!}M_O;$zMHSDega#d0+(=GYUBM&x^X z69kD|qZ^@FqD80tp;yAblaAUpLOhbXWT)^GvfK2IupZfLy;oR@tlY9sn2x-^!$gQg z=6QY+dLnNh+$>y!yctv?oQ1p|MiG#Z$SmeccFTovT+-0O76nQc|MPQ9Qa`P=; zjPx#`^FJb8N(}gENZSfmzAMtWHk_wI8a7DDnahDRLKmFDEq2PfZAnQ9t8#`6AS}%l7ZdMra zYEV~exACG-u?@$0t5A_G*SUXDLG2aX9F$`xmg|Bt==sLs#Bc~BoYI(15}><|*Bl z&4{_dPhv;JoRt4&=EWQX$)ZbFK%XeMd(qkCM7Paeg1w3BT|5Qr6HPo3f`wpnsk?km;?f{({_jM?FeJY=QTNnyetP2k zxB-4Z;;c(DzIlQ=eHo9JAiQ~)_dbDIP{6yMFjS1=?MnFdgu|PWfUDK#b|f@5IB`=F zs#`+28xtP2r*hzg)XpMKX+lWPbIyT;t%Q$kd4eISk6n3zOV?rVy6}@9#`Htgcjv7L`AgGOhQX|2L&~bp!ufO6$6v{HB!hP4D>+ zQ?j=I$~&Zitr zBk^{m_}*O2o1L<|;5heBic@hR_il>ylP_Gq6yqA0tCOPNu#odJWmby=CnH(f9>m#} zJklA@W+#8_xyCL_E+srN> za`iGxio-blnRlL`Iq1yvnva~o%*6T$&Z5ldmIZ8l=8<-Lc53GKP9OHh%vC+dS)>d# z;W8^XW0Z7{wK<~|{>|*mK=W5K{W8wUuh5AZU{=ZK*)D5gQ_V+L-TnX3f5|& zcp-Vc3u|}*d1Wh2dAss**5>jy=EZNk$Xl8hv<=6d&fC4~9d{znZ9j_pId8|IX>LW{ z*3iq`lsw0%pWMK_jd6>(7J1f}TsZtZi?lPG4|%JyUvjeY4D)%MeR=bWZ8!^aHBX}0 zzjK8(`RqHnw0bOiPwsH@1WS|qt6iJ*Hn*|Uf^{kv-LsXoIQI(S5VJk^DCq+8bgmGcX;w(`i?tb&dQm7v;mBN=0iHDD;}Cd+FbAApIWoHxewgu9p`2~&|aR; zJ&zWxDdL8q$2Z`(p6GAe)^pdPn|5vG>Yy?EG#ozq<)Me1ZgfrPO3q94v#86Q8|Wuz zJ2-*phZjX0WAy#BwQM>%H#>&ih|bD?$G(D2c_3kLMPGQbnI%I<*MzaK=#cuGtP5!0 z=65V>^iJFulZdv$tC)Au3wsQhZbbsZMh3m8kL1rNEP4m$G3<-bJR-fRC_%m*&MN{l zd(PsgV7|p!{d5(m+bW|W1m}FkdT0*^Q~rA97EW#Xg{4Ii4>NPix|*|@o@F0#4GekNb9@iu zRoR_xI^$^B1;RWARCa`9N3Sm12FK7n%U1C|z`skU<;!S;rT;*J!t-S>NKp8^ECTiD z3nr*nYug|rHnwIh;sLv(`umdK?7HeV7BTGm)lckg*%zztIFGZBR$tkJK% z3`rY)Q*{sC4f|DH;uX_Is*cJeRAm*&HnS3%^g)8+@*Chvv9jK51NC{MIi$z>SC2*P zW&N!;Uc8j`p?;S6HdX~jXm^-(9Yb=qXGLJTJ&IVKm>)hxthJc$frnVLF`q*e%n8h= zlhMo%n3mWt%v?;<#aYZCOkL_erZJ{AtB66vJk4V<>M)NVY-7Y@?v>wU*kW!~x6)~t zR1B3~fw|DUf_@Zp3g<}?yz zOlfv>N@N~uUgNG{x;C5o=rB#2R|OE6Q1h~o`;4(>!;^Cv@0xXD&oZ)`=Os2V0-9&0 zs2MA;nk;YnBvzhRO|QiY&~o}2ETi0qz6MLKPKAlsKFmw_9`-AC6yA$%#?7P2vCr|F zX)myO-GQ{j*o%W#sY+}pv4;8tyN5=hx?#8Q+$evst7Wy6by(0xF`j*?0SSudUoL@K z^O*~3<7X{sA>&JXyH*F|BkqrG3!@76!X%z?3-{0_k#Poh)3Jf!iM#B6gkgod;GM}> zfQt(_NoV0=Le|hbai@?~^e4Czu?F;WxZ{c8bQfGu3WlzY^UGAg-8k>O<8Tpf7y2z6 zh;u2Q2kYXtRJ+l>Ev#)S))scQG})ZKQiCbz|GNL3hevTRrhS zCAsYpt%0(#?G|t0)Z4bpvgE0CZ6GC0ul&gY*@=pu9iW!~1P&0r?8g~sKK)78RxK30 zu+v~6nVyVito}=nz>ip;rMu&QIMC?J@$cQ}UI zlXJg=90;7#w-|UlS^pTIru|C<_2S=WkT#6!O@r8QV2|(IKd?uS)k+z>wr9oqL$G$w ze1|2piEf44T-xVuvDYzLWw+4(AT7O{7i>)n?q)@{(d@fnlp77|o=kW|{nb61ET^Ko z2Qs~=$GdxS>!~K)U1(j(c=wmG^OT3(AF5tae7o!FhAB{Y8FukhU3YGq=Tt~{+V|9{ zdEMv!yq?5%hYk!)hID%q^(PeF&a{AuN8RhVjT27Y%Vn12!R}cgdk{Ce6QmB#jIIFn z_(%h&Uc*92i)K4ShHPotgu64IQ-y@E<=?18!r^rm)HcGNEzZ=31Si*{)T@Mzd%sYF z2=;#8shbG4$6iq96V^p~Qb+{Lm~Kh~!6ac5C7G}y`4MF|VQ~h7GLN8>8#MKsFt_Oa z)NKNy%y`OkP+k=>sT^e0w*DKzHTln?3Hg)dWvq(jM@Cr3#KGj2{kBYEGEPeze;6`hzgC+#TxF)>DRsM4Oe zPqM1pHnE$u>|GjJK$`dI9l3(2{w5&%5`}-(k@bjE1L34W;@|N~(kWsml}4N*wsU=n z*NAVWzlqz4PXV=;u^s|a&triOWNMfNeBY^eOz_m2y3PQ*)J*NBMQL5c*}1l@iuHVu$JfpFCC8`XTb}oP2+L!4DQPD*)*xNe5{=YeEP{6Q41jT z<)UIxvqd{V4HcGwYASdK=}ipqD-j+Ok9qRN%O)~;QB;Qc-pkSzL^kjF^Dtr!ul)5BVlWT=mPgd)<$MAwka(%zE{sR;5`Nc@&*h!&=Zs-^ z$H!gAe0hG<>!TFjZVo(}%G)Z98{NnQXB|$=R{>XT0-*vb$P5+mcgdfWFF{RL`~}Xa zll-5SkZdZ~(a$IA$fV}aNfMcG12|At#@W7@R41c*YLjluXa`S`PRb}pZjrXjrXu!{ zbYd0Zp=ecxsLlMMg(tC=jVv=&9b)F zd&gi|>)Xn)5?SL%c+68)^UZ#gFDw5YH(DYq>TeizlHDGkG14nbr|ua!CA-MM3`=Av zsq^q7S(HXTp7I}a8NdGj`u`ot#=%+@@RKk9Q5Yg%W?F@$KtN&qN#INt(+N`0e-wrU zXvj84(n3ICb`b$@KJb><1t<)cSPm%6E8=B9Vd98;0EHPOE&~+i)Hn%H7|b|WlXly4 z9ITGHhZ|o9D9pjJNkC!HW2Jz?kj4Pndh0ee15lX!QNUTgj*Wtq+rN!RwE=}ej)1#= zta$|F3@E%|FgDKN;T^#H_%}2PD9jfPpc0GBGzDN4NuuU9I8lYI$pj~={Lv(Xy}TZ2 zE&%8MmL?iF|CyRF;QSY8fUCc|L<4BT{vr+d1xnD{^{)88iV1}Ps0dPkYYY3xB|t@}JP4>Lla2uuJz%u+ z=QV2*Ank>BHSv&^L4zg+P?#+mK)%e^XhI;!8u{P#-;Dh4`fq=%0dJRGm747kw69$Q zp3Z@v{~LeEKaC{>AwO%3Aqa)j7(r0NqyIjI19C0vj6r9?D(Jpf2HfT5)hnTGbLbE1c5>hRGe3U`vg9xmX=LQ(3jqWmVoeh@$((HotO${}!kp8-MjWeXbu}8BB z(%a^#v4Qk0(ItrTz47NT*S(;z2sC6RJE&r=tv{P&V&|{o+^q#uJNs6HMD>?Do=$L$X83Bf`gJqGzS(PgAz2`=grdk zrCBy(|2%(yo|N~4d6Sna0KLjm%xb-@I;^GWIIEhi#qNq#zR@D~x+;%r{T*^r z&eQrr;wWBdHPMX}u3A-me|fpqV|lqWR*R^iXuMZ#gl1?QRxE>RH4BY)&Mwl-S+Yhy zL8HBxu`*pVd+|HVYR$~WXYEdCpv6laC)5hVS8ncVrs0LXX!T#iGrqm*X2Xc1E7T>1 z0TDaZmkm8n$Etk|H=j#auQfEgQZTJBoN?{`beF+c&im;H2K9yg(3tiPKgL#dsRWbU!){QC){kt98m81HWT^`Cpz1O{4mF{{jLv9MTZU=e00EOSVSL(zo5Yp{>H5#5~r&TMsvo@>^hC(#6E&ONhQftgJ^1L0PbgRS6?uSpSUCi=NTd9rA zlF#j$=9rzoa%j5U?8LS3>3lQqoSV}}&DIsxOq-i6D*2?Mn{q1pRduF)wM^AH)8+=X z%Fgs@tAUbkda=VyS!HV6Wv2`^X$E(cu1W0BR>dci)nr#iv~fTElwy(b9e%z1s__ZM z9I25>mPV{`-c$}$tlfAWTB7FJeM8JqkJ&BPO;z{VPOm_zyKLzecy+67pKZOm+V-pC zPIaMevunHhvTftu73vV%SH9uu9k$PpJXNo>EeroWEwe3(7Ek}Ry?su1`lW5!6^rRq z+r(>|rhRNr<{Y0kunjD{rW&zzDJfA^+FDh-QAOG6)&5Yewb3*Xl~fygi$qyz^QS{w z8ERA5wM4np=3wt~MUTy_A#25T>n`$Ug{^e~Jxtzj9nOCt-)X&5fspX6X&R})tYn?o!JEw0rZHp&QA9LDxnLBOcv?hI8C3jk$W3Bq) zw4l&mb;nUra#rQ*2v^)z={pY9K3D#8{MPV6dDro6OP_MT;Qt(pPMDz=0lO$|0BTD=%;e;g`!H2UodV^2MPJs95Q_9H^MPdmpqwUA5Cv>z;a- zyI|2f^#=FO(=RC?iqWYPdB=!9+XbscfWeX zbNak{LinZW{qFJ6Wz!b!r{kMc3it5KZK^-+f$85=kKMg)id3iET?$vL?A&cjb|{7J zt13d3Z`~KwCMgr$)D8EP8{F6}7zNXf(Ed$P>(LDqN-GT^WdB2-ESzDgt z%BOFYJGp-4=gaC{%jH~2n(L58ra>G4Z$&lZz+Py+I%?l7t+;8DH+y0B^bhZ*W#^~c zysOO5PuF>u*fdU;c;9x2p1$UNb%$d5ly}0Oxaob~@dxqKw%%utteBqfeLCDvMfE-z zovLc{4vR;t(B4NbKUJOa-k<(L<>c*pb6AD&-c&eC`NMlniIuXzdufHY(%*Yt?J1?6 zm#QIM@!N~jQm(k^HQfGAvD>S@vr7Sa-Rk)(fA1AZfaT}C97ziK8n1bDd)dI=Nq&yZ zfA3d0Q{uFjsFA75f_?%Ou|ZbQ+-c^a6fO7ZvjNW+%$zu^!|W@W`ol%0UkE) z(`y499CW930@m(mQi%i1_H0%S2N)l`ulf+MI*6@$7_dCtN_8>7DEgS{V1QnHqRKL0 z{^d-iEI=zgUy1h@-E3Fh^QRQBlp+4ZCHhJe|6dhb6qEk#wSkIf{!I;M6%qb5ExC&2 z{tw!#1(9oW2gAnq>0D&$cf^lW7{-xl_dHP1gS{TKG00n z^C%+pSQUnBnf+4bi;P~fOy!9TFilW7BHgUFsmzhKTU=H1ktRFlDEY{hp4G|$go@*$J$?S3K`T8Bk#y4|*Kcs`xt)q?E@UgiMs*Vx1B7$`7a> z!)#>(s@3GN@)_#c`e(|!sDjOSWfCgY<+(Bfb;UD4>4~~@;IDEG>T-~Wau(`hSb<^! zb>Y-+#Ya?JoJesGg}SV#IE6ZyZmDoW1>X!+%t9R~D3tf0+>1ZSAD}irp~^#0mbLTb z#;9ctRx%<=zr|1X7&W*3oa|T(yE9j|IObPRskA5N1))`XGbV%dPwEnLoUSk7#BAfA zl3a*cDsLB+#>~{nmDX24FQ!~~Wge)emxdr+@h#!~Zn@%RLd$_{MQ#E(fKm~gfDH>$_$0hLRidy+XpH+JS0-RC zP0D)`YSWbR%7iC39p&)}_X|++?Fl)>rE)|<(i6O_GaVDLRK6+q%1fUBF{*f8<#5&NzuN9k=v(erTvg;l9e~L zWq*=|1s<|T$&BJlGGsFGNwsW!a$n6KDL1*RUMOu${?xKcdOo?n-A%eN`Cey;gqwW6 zCrA6HN8G=dT)}&GXMzM2Ji~Nr8VN#_=A-{ZU8^{@@=8S`ycXJY2 zD1Vi`N2^}`HB+H8DgTt&xhh%SnAv2NB(KbTyvbaimw9dbUirn$^SiC&$1~6E@0Gh} zo;w^WH_40-{UK9l#+_Ow8_ql%=O=rU8GGrHEHm>=TCwbK=E>|qnMLOD0z)Z3Gobi@ z^kb&ylk3vV%&j#qrQVq~^*z!BnX8(Wl0TXA+l?i88LZCjl6@IJdJaku8MTDd;>L_@ zQkFP00|~c_XJokZ^+lN(E99|4?Hf{!QdWBR2I$kP?}Ax^>|5b3XueF8->r2i?mP|p~GRxu&tr@Z)v}m5E>;U@H@;sRf`t_Pe zG8^=R4LI2{bm}%snHD;B*G4G=eR`iv+JioI2rYesJ{h`HdKVoTl^~5nhoAi*^+E?< zWJp({1JjmEIOu)Zp^{H%kNg_R9kkN}SaJxx?un&jIeKM{pLhhVQ=cR*LCeF9iFiCo@QFI`eil9fmpo+^Fqn)7j2*> z)#gKsCAexHB3IH;eS67Q38p&H0tt?ji?FwpT(9>_b(_0o%8;>*?gsW-*DsuyPc5ih9L&NmbFRYMQXiXK); z%dw*1Do!;`w5)2X-cUGDHPq}Syj%4fmnihAYQ`4}lvU;3Zv-!^G6(wwhpJ+TD!!`f zAiS0Tv}z+Sk8f9HAmi}tpNllp;sb9;AVG2X4bUQveKQNx(gqmP6OYwjL3oPknDNE) z#RHfx<{QKxF^zWn#TA%R=hfmHm|Gs##HTS?J~zbsG1mh<#kQE#5Vm+8=IY515e1VN zixqvuoJ$moZewCnw~2xI zQ8sqceM%IIC3>qw;53(jZqY_8AtXzrgZ+n83K`h$*f3!S_D5ocun_w#g((ch;Lv3*xZPtIr8+u%(!1{3&b!c9>s^&A`p&`(xwroA|S^N4x!b zZ?W!!mwBOBE8=tBEbJm0nfvmcn757V_Kqlf%E@~76(lI~za)VK#l0^e-6<^p3|tCf zbH{IJp>VwYrdGR%heBF;P(A$W&dow#3+fm@dHSm1{<$fOFE;}+(5^GP^u^fUfboK~5F zAK50a-o!U<<71+E18tMoC%jv2|Jr`=cC@vB7jwC7uey!6f;U|=v`!0t;0X(V z3qIiMS9b|2@fFrlf@}B!hatgne41OSz!jh5H7GE}U-s`3XyFrrOZh~6JaP@c1&=~K z=I7(16IlE({E3vE{Pp;d%zT~*e>hjf`+z@)4&Zu4R<)-`(+!h5xe>>HsHMG;7PLnGI;6>_QIg|7wnP2 zpwDM0vV%#Vc2Rl;uYa5)S2~zTA6VEw=tuvpZZWuz{z*r6a1H&X;n3hTdX>2`caZ*c zj{)~1z2eY9ZZZ9_^8;=){lQ61?jd@iUkq1+P7Chh@aQ+Av^g*6*AufivGi*x5{@}N zG259lkseLEIZ#gzEoKY^(@$3@4y>jhtugIyr|)eD?Y~4fZZ7UOps#EF-q%H6z?{*S zK%c;}?$c}@;S~2aH~%A+^tv_U>UHjcpE#Sv-TwpU-niC3Vo;0kjT9<($G69n)!cn; zN5f#?_a-raSC{r0 zFk9-mJ$=k~%`1DdnKi9WJ$B5J_WYjl%&e}i?#s-p9E)x>W)$x&`!~}E=bn}O0&&&B z+&-N39FXfB!>avYFV1E5f9yF(nb}|5y;0_Te+oNi&dUC9c9QDleh+qxmU;gUc98y) z{uOLL(-Zv@*gm_{`~I>|A58Ci&OYU2*ms5P;ojVLjP2@c(6^556ja(PWFLu~+*`*! zkPzJ)$KI1X(reDP$a3lZk8PYs>v_r6EBe#p&(aVZGzsV4Y<>$EtnX$aSpLe;&cv=)RXDN+@j@ z=XCqRhOT1m-g+=dW=o$c_miS*-*j%(@)f-TZpAwD-XGjzy{_J;+yaw|-gIuBm892~ zn|n~9*Mghl__|kxd(-_;&mcF`=WS07H!aAxCysk9;%1LEH!(q_XBIa$IiRqo#I?u?lmxXD(J@279nByJkf<8>=wuyRjN#yrug# zR$(4@hhi0GpnDHiVT!v~Vim@XjmPo+%&x~Oi~~Cnt1$Q2SZ9gf!dAp8%yZUPtitSI z;r`q!EEBB4eDC686=ri6##0s7g^zsQ)V@4s6BK|nKdB9ng6MzOWu*Hcadrpfq+8Mts~tQM!NF?@F|qN zYNUr2kvcsfb&m&pm+T`$dajZ5f(B{aRKTtwx(4pyuLOlbCjK7S2DpUx7ySp}c=sI(D-@J}mjUPe`)D}NPZ`|^D{<~4 zlZ<@Q|7IaiMmuu_>3kaLl2fGWS!B#w$E8`sNV71KX0eludCv%`-Jmp!aA_7!WX#^* zrCH=lvzSX7I}D?kMH2L57FO_=Lb=I-Zm&6(NqWv0aBkv!J=qJfxq$O`mra3XaQ!O*SL}->*pGeTOjduyZ$LnG zpf1GN-j7#AmrV&lC(ogvlT`dLwn;13kgioiCvUn$YP6fwJO$&NY;8n(@HDBT0>&)w zHjDIh5k@Y5_7aRx}nvWXJ`CGofej%$CtlH-89#to*RmQM?GaoLp@E0VT|f;^#Mky zfmnDQrUpB_gF$Md>sRQbCU{)|HuYLSF#M&agl&L!YFf-PU{F(%#PErFEtL!Js43ZB zp^kckRtC?h*~J&3lv-XM0r#m{)u-S#HKX1U(y5o5wnGy2YReXgqF(y74FaiAoio6b zda2(U4pE~<2Vp+-wgkt_!c{oi%%4|JSp~#QuFN?grp}*g1;muNc{OO>QZuw+l^O7+ z3~7Hr*#>i9bPCtR0fwe-u=E4YR2!RH&^Oh?J{#CmMJ`3~cj}nuWne0-@ehaZ3WlMM z@KwP&`T#U57$jNYPnWGrwSkukTe9uoxq>&%5=s@~iuIsq+Rk!I$Wu69Z2?&d?)Cd2 zSz&UM1;i^XXgLTMrV4*~Lcr9!omOyaDzE<*9G;p#wgVIuZc3hlU`0Bah3ZnuOyDiq zL%j<;2r)Yb_kaDj67vLzrG*N**C8daVo?SPjmjhMwVmGbOEP^Lns z?Smqfy5j9XQxTURfNYi4YCA|(`BZ-bl2o*soFGbNe#;pMR_^|l06xlXot|()xuO3( z98j(v^MmOsCX#=^U*~{X5NmPv14dT|Q*B{HT_7I=!|KoHzJNjXh9!x>QU9j#7W&nD zw4I?>U84U9*c!7L?a#nBjk7M1(4vv(=>s1%uK1sT_ZoSj z)=;l;2eYWvD8asXs&O;*2t3xv&vt=Ajhi$_$kVuAybrQ9XyxvZs!>|)1xXrd^%o#o z!>=hALN&Iwq`(=CCBN>0hx%~mWpGsQ?H>kfb;cN08#Ux40^sZ7Ckr4pKcdLN;6@j! z0rcstncNFK+M>BZ(5)l8#1~jPGc_vVpUz5cd-$WXR{uRPb@rL)z)u|~ODky8xo8uQ zx6#JiN5e;*2QC@#LFc|_C^YI+`=5bVI@O`JP^HrteHbcqUMJbXL!E|HFSw`kBHJJC z=zOLHK&H;iVmC_J0xA&OgE0%W?F8CX2UX@^YG5F#y$ob48OWuhnI%mJ;R{NknQga zm4@A+d*PAcQ1m`1G~^`N0?m+<>IpXuS=m95ZrDQ$g)4@Ci~S+akX4=x7Yu(@XTv$e zFZJc%V_4Bt3GRlsTUx-;@cgeqIB00s`4g-R_Yc^Cp`jU1UcxuDfr-G|rGr_FSR`Q< zT{}KfWr1PVIB6w(HM=s024BpsFBZTjvwV#xXf}JMZ3-XEUg*Duw`Si=Hba9M%hCy6 znNP5J05#^*>>tB3^OY{2pxk`5=OcJzuJ2z2MdtdU(U5PxH97%u%=MGv;fA?hDh-m& zwX=&L(R>YFqoT~!i|<0H`HJ%Q;BUUT`V)AX%hq$i#jL-nAMDM(w9W(@vkF{0vdb)~ zYYA*OyFKs>G|is#!X(ekykR=@?f-*Wu=bl!WT9pn_9#TxLh_z*NmY=vXMAcK#8~sQ+u^)5kJbhFRd~fez}vdJTnuj3 zziR#iC+o(BrC?{B)2s@6tS`0hh8@;E?We%V`aqW_Y_z__Sqt;6FY^9KT&z`K0kqj4 z#w@-b`AYc@-X3Hu~V6C43&0E7Mnr2-D(XDD7D+Fy#OBCS?bq7v7MWV zCET|QvW$fSyG$E4^wgL_Wb z6-}YQDRU7U@||+khak_Xe7zWMJH65`gPTrFll_qCG-`PR(w!F{P=FL?EeB<|;=Ie% z1`?e2dToXnXV(C82zNdnwh%&`-D9*Nz}fE7YVdZpO|u4fXVaV3;Ox9D-w_Tw>y+5Q zerN59Fxch1vL+5poRu5Oz`%KN^Jma;UdmVqtDTjY`(c?A58nqba2n>$mGn8)6ItSH zryZ~!a=pm&BG;pmWj17Z6v)`abq~#129WNtVv!M~dT6TOf@BZ9^)+zC!&LtY zBzZWRY=L->0LxT}^|*duDnxqRb65uFJzlxGK(NPKFDnS}_#SW^d^~=J8Nf*oX3Sx5 z_4s@`tmy_FkAID-py|P&8-l7w zJtGp7Jnk?bf}+Q1)+kK!FycB&K6_A!kKzsPrLYa+0-j_1aRF9njK2!rp&RXIC^H9Q zd=qC{K(ueLN+v}5MyhXwaNqRxiE!RGN8cYpe4m+Y#IG{`Vwnm4z9R<~gRkF2hb`dc zx5PCL-2F7X{J_P}AmA!E`Wc3~!6856n5$spr+X<8cKdBks|0huRW~bPo8QL#@1WnNYwT7_V&!W)>l>AoF<3Q1G9HR=R_%<>BfsF4(wu)rH*O+@&lIbf) zd=|a&=>%)=IzJKPKOKheg~2EEHbnt^g9X$+boTUK@ClMAzX9*y|I~`XD_Cy*bT}0} zSziU*gH?=s!8LfJr8^u8K48O#qroQ~)ZlP%lxqms24BT2_66q!q=Qv3EzB3pgUe&` zVO#LaOAo*x_)FSf*bv-ya~ReJw-+n`wcwu*R>6|s)=DQ>5Zq830yBeeH_~8ous6K| zD8bqchJ+KuVU9}P1r@TFOTvO8xZaX+L5GO9qP;;T-~4veGbd!2Wu$HBl!neFU{A&vaQ_Y~pllJ}5=m78t?o zD5D2^K_N=DG7MxRp*CH@k9^noSi+9HNUxSuNA73PB>|D@%x;Nle8J#~3C3@Dxfk7b3Ey9XS<*RbFzie;nYs~nB(7Ol0A`6=D>cD1QEOc{ zY){nH+Xh<`&5T!pVdAk}t)Q0}W#b1M6Laj}gLY!As~W6L{OYw8G!lCQPJwFTP}oUW zoXCsGg9VBGm)?M4;_oyOC?vkQr3A8xg$4Q&VPf2aeUgDhhf04*Yob&`I4gIS8~Q2#Oz89sY z%wP#cTar249l{UE%|yQ7axx7v;J@2gBL>;qxRMd34dY~+h z&E8tMRlGG@tJYpbWY2B%6%}WR>EWWISxm-t(Uh!L%qPO9S$9}HLg%a~?k2%l)-mEb zzb|VGlt{AgtK$la{ChZCE2+J=fkKsZ7YHb;B;tIpapn@pUomI3M3k?%tVqJkr>;3D z8KQM;ybjLX%S*qTGh>G;+3?D{6C^DT49MqbcdExsVcIkU8&tFnneq2 zv=hFg9ih7mLugwV5khs^VrHJ8l{SvW5QOLb<*pRW%X>{+RBgco^};>nq%M&Jxe5IJLJ=#IHDY zI8Vf=@H)O&^tj^msVGr=h0mD}B8LjU&}pJo6=$NIguDv>q^H7~3jeez!pI80oBM^P z6{qu~g%c{=OYRF^S2$Jt5}dEtUn3N3uGrGJfInEVoW7c$UonZXlW$wjVfyeVlz(B} zCCbVxI1*xic_!h`E5uK^eWDq4(=dy9uW((eXw@tH-zVDl(v~tsbos?3YQ3necG|R7 zQEpBDq6krJO~jV)sbvI`TOlw}|?-Wd|sVed3 zH`Y{C6!9Z!?$tE&4Qf&w28gkmF!}_dsOC6h8R1Z4!L%k6YSyt5cr`T(I4s`rnsLOz zF-G+O5JL6$xN=sg`5xbg2sgbeL#^I^q)Zn2y-A^-6kcz1ozf*tY}}&!TjRb~aex3W|kaa4ZQ_ zzPO_+KI5FfK=<<;imc$!M;4`5;75NT_e|hUk6p+Y?4^gO+X=MkUOH0+Gw4SRd-$XD zBj(%rZS*61^!QKdhYu~`XV4GfZ}$bz51rh^x2D_qUFECNZG-y>9^EF&fM}raP0S~* z(RZiH6Lxf~>{G-dx+U#Cuaj<3%;Xi&%_^4gj?wjM_V85bYZ@ZQzR?#omyBJbPi}1+ zGix4ZDvT1%-&ngw^O|3B3P#PEi-`Fn-|+#rR1>Qi#eNHj@r=UY^E?KlAo3$GpOKen!1H6|q~!7R8Cltrc;gxAv>jvb z7}tuU#u6D9%bUl{8NoF&W8)Z}4f>;1jHAt`M?D$Vt$CyK83yegBlQebmeR;c#&k~L z2u{iJxWf-xhj0bOoWEE_B4+<>LKXj#tDY7AU`E8e-?u4>#QY8ynQCH6hsrz|p0H!i ziVoi24u$m_cy%4)419R`Oo=B;4zJq&p=%rW2Gv2VK{~>EG2otju)gm=9Gm_wwq7Jek`;#GBq{jH?b5 z`j((_y)RKpuOim&$9hyKGspVbVwte9R<`$?#bfns2i5qohwKAdhGWTW3;p~tf3~sd z{;`AXt-GVgblHXn<&1i<)gpMKo7qbfd`Bs4 z<>bF3b?muWHX~8&>3L}*#_UN&jA02|rd(~flr>uII_%2ouD?4xo5gJUGxVDEwN+)v zm(|#QYG^I1qN`-^8!L~aFnEl0otHAG$ch8eC~w3Y=R1c-tkLe_5%k;e9<+YQ1#9;Y z26xFAj9%s5nMI7A=O!&*IO@TTU1vDDiyNlbGP;_3&LkHn*aEEjM|!wt4pK+zxjv2+ zBU#+j?v^9o+*3YJN49d^g4T^pAyVAhhUXC1d`yQriOYet zLl23Bi1|Z-L`;0{&=w**S$T*`gk;_xtR(_+S%V>jcag@R4&hN2&FvXTaXA*m+OAC;Sz^h+?SX7!4sZ3qG-5i4xz%LNh5J@&FX`Y4 zsbQ%6&}6K_WDK7Ji9+gdDo7R+!{H!Vqdx2k5?#$}D7N-L}LtjC1 zw0NipByKK4!65N&8rlhxK%1fIAPK)S_#JSu@n9B6u9XfR0ZHbz!8urkiRWUix9~GJ z2CFbyTwScfgmb#F3iFARj8&Lf9K8Rf27R63dQyqslyvmS9em+MpAzr(vS?&NNJ7KZMX*4j8G^W1*BV|NX_C&caJAM@SD^D3jp{XaXeB_XVNpDNkg!1hgsYr zO;}5M?S?c99Ff=;andYgrCE4Mvsgx2`$(F_R%sSworuz!C(XiAnnjf~i$ZA@J8;~a zP{>xzp}ssy z{)LfGYOO>kx4V&c{=qEd`?1bKQ5eOuMc$o)wM^N`l&t_Vw&P~wE4oj!KsU`FBV9HX z-MlIj)zxN@>bH;@W0jPmzbg+l*iYtVenbzweT+Pt(obUTlXBN@JjVGZs2pvGSd9L> z*on4X^#KW0Hsb(@sd72B==!@Jqz~m#oyxBuqMFx+qZ=DHpsvj<5K<4cenro+lunGH3N*7-N`}~_ZFi8hsv=v%$<*dWCG7)6G&v^ed<6m@ls$88Wp|}Byw68 zF)ul#EB8P=>Du+D=>43nAf7C`iz7;2=D|x4$)S7edu!=Z4JBtl7$-K z^609#Q4lY@KxVNp=6WgmC&v{1dsiMrN-hsRpgSv{pd6gf5h>+0?g5dq5`88Jm44$4 zp-|br9Y>dPWLGc?FPow#TH=Dkg*4-dPxQ<~H8lIlY!GVStHrZG`(0xZ`htEE{lhp50_{&s zZIsjX1s&o12ZWCPSP8(^A7mC=Cz0p4A(Ns5620H!a7CfsiCKE6uZlfdsJ0vZwXO#w z`ZBtgQG;zb=Jj27iqTtpd(oOh0ubx>JC}jjK-S|5y2e)>H3$@;N5d1)V;8aI4Fa#O zLVYu|(8QegX!PAH=$(hVL1=L2$rki=tr&gM_!Ip|zYYR}my8GK52hi~?=c)Xrk79R*=kzkn~OFCd7@v#1t2nIU&J*VrtGWfC_f_w z<=)1OOyO=12u%eK3qfc$>q#;?>4gnC`;9sXOo7ep{ z2#Z9tZr?4m^YC2|?_O~10=nkpZV>IZ_2r>QgDTO`h+Sx4{0cPwDhoZIf$h9I;r3ZH z{GJwi`QZo}`{V}*cBj9%i3Yw2Ktn&SLp|Cofxr7)hYWfYS4Hu5U%*vGUAsMa*FbW3 zBbmja2c&kJDT_dSKrDmt95_Fdi^i%1ptWjvRX-rn!mEm{nw}eaaytXf+=V0Dwtv3~ zhz=?q!S_4|Esw83-A|69SN#gn{9q@vB0`8(#W#WQV8ykI=(8KX=%YK6$nAU8AUIg~ z2v3^KCV#UB_9}f^7t&2lsJ9;KxBXf&$`Wkr==D z7?sp1jiLzRqsL^nfY_mKCXNk!OJ+JJBLfS@hSL@91C%UM)Rjqt>C55;!36n3((smC3{vO726sGtmC~&(WWc&jH{4 z=Q9)Z<*V`NgSQ{huusWAxS#%Z1Ks@l0IJ!&8HU_v4xWZO_k{!pM1J_*7L6zMwZZy@ z$omYH0MY6685sZRbme9co~El^2cfs!`U7a1zA4&iG7f}3D=l%4Pq+<^6Q2+E7_+aU z3!r+Q_@2kt*B`GfzEPoTQCid(dMhy(ElxR!re)!(D&N%HXJ}BN4eD2lR~}#6XCG1X zSFu3&&VIK82%rAXlhIe-<$>pu^ZPZ7`S^A}hJGKn!M{-HW68(saEKnpFAOHnabZw1 zW+4n*LLCO-xq~y7qISw3&_uQ8Xw&-js7PN2A7f@R5lyo61wmlv0T~bksXAb*2Kl&J zp;=zj(bj^jm2H5J3&kapg%+ z_A3z@^6m-nf{Z^0!dTGa?`L5sko^bOYy{GKbl^u|;Sg5;18D;MKNv+;kpxlXh!R8^ z;@#4M@M~1ON?f=%-4QKT4o5rGvT*K*Q`2++Jdk^@bHytjagRb~) zGg8B=2>6kw0&opgf~Rr$+!kev+{WwXsY`A0F6?iXGzd2zy;e z(W_nDMz8Z}Q-MYm+XMc2^WfR{M8h=8$#;j&y9PWVz~34IB- z>j?OlaPEB({7f+Tass|2%=~c#-o}6Xy9#RKD|-##QGCL17{te)6K)57>LW7#RGc{n zeo835v*lltQ2l{__080Y=xeMc@vm}M9>e{MwX9Hcz2|7AaSz&MnFajhEeAZ&OAb$f zNbWpg07S~F)7q%_*<0Y%JW7uLTLJ~idwc8PMzZ$ELZ-}Er5Oa)^+?=med z=cCtc2=teO3-GcuT!k=}<#T!tT6)$VMzea)Z-S9*6-92``FFT z@H5*rKL%Q|50uP+rfj|Pr%<1*Tzvqlv$|jNp)Bj!hg>Mk3jOK;ce1R1TEX?K+5c9< zrOf_5V~EJC9k~X_Gw+G=pf4X+P(pt``Tm?sYr^;Eqj}+!5*WKf$i@MFk^a08+#XUr z0b{u;Yk!~{bg#o`uFLkHXy&f>Fp~SnrUHiYmN@7FC(qmUAN1xuIXxR#d2luW{?N9b zcY|NF(AY{~&ya{{_9_e%lq*21BB(J|0@FjJo~;6;GT!ygbus&WW@ULwg*)fCLT}-r<@11Dcxp`nbQWIRTnT>)?{A*~9fjX^&4gb? zGi_wyd(lpN8nhNAx*mW}MIXF!p{aQ4*_rUB*!uh`cvXBU_9{FtZn%6C%8Q57UqVUA zj9Up%P@-1g3OOY!A5h?WiE_nbxKc8{W*5X2f35F<3&pgiYjC#sc*{=kD%Sp`4abW{ zIv2y?qK1A`ur5j&y$hR*{KW)RRSshO)s>ZKZRITVbvd6h3qF)xlyQd7rK5_t>Zw$? zv>lqtWHrCSyE3IsBhXN`Y1?6_E3>zVh8Ja5_eVf=S(UvOJS`h_`3vRc8eYcmu>3^8 z3n(nV6UGHvIWyJ^ZdJ^^oD3NiJJQ?WT7}=OCy-E)R8S6471j;3}`3<#C;vU{r3?{}FV`HOCBLNx7Qj1>AWNijm)W zLDq|DwK?d$8nSl(uo}O68_KIfW>iCImG5FEJgf>?od`u$mo~P;-KyJLcL1&G#m>8M zr>b*bCfur)JK_zQ)$3dqLVERK&x>%aIzB)ilB%DD?S{DOzL*w>s8PAR1VUTO7=*Hm~2 z$@PmCErBcbs;dGav0i7x2*lMJ8TmqV{r(*t5K(`6?^g(`zj(L?g6j*9d%)THH>X(O zThH}(1J8yTp-;fAK|f|QI5!--bRCW~TuPq`2O6H;!is;xpMpRzZ{#8pPtjJFgs*wY$; z-`rx|>bSrKtXkdFn!%#gQ@a_=T0IRjz_>NWd^H%gX6~5*2CdHzDZ%E}&(2Rkr?v0o z4p_$+@Am*!F&2avfVC-$aBZ*~fX|0gxGgRB(iU$}nUG3t`)(*}>u|aDMFG%#Ml?LZP z^UpVoT=NgEj7H5!f0O;9*%3&Y3u~CWWPZVFrtv&C&|q4w+zBh0``0JIGUh>pLRidn zHrooy%+uC-FrOK0X9=^J(N2G01~bVc6s9s$eA{6HGb7kSB4%bqW=n>dS&0e~HZvpT zyyPeIN>-Poi5X4Pl-y;WFZPn0VxBK=l&oT&ubC)jF+&^7#kZIt&Awu5W?*ZoNW}DN zuM`z9_jSDySu?dcX1QJ$bYiazD%-Q4wA&6>*vhi?WMW}F zdmLVGsVs@=TZxz@(DIP*SX}*D376Gl>L%%7b?uIo{A2w&m?8Pe`tGDJ`O5m}{!#Ld z)!?fuDPp|}Dw2e=UPS&U*~6+%xFAtxJx?aYU98Hilj0&)Szf8wi}k38Cst=YD7O*) zWffQ75?yEAtM3!-V%=?CE)=rvw;mG~u;vXm=l)h-#wooFi*|KUz>EEZRB|8utxr*Oyll!`sL)Sy*jT@FMZkH^2*sw|{C&iFB+#H28_~+dy41mr+3!xFP!c&Bk zfgP_VXoj9^G{I%)%iJKaf`QyHfjkTrzTwxyaG5GU1V*a@_-ldpx`F5be$yl(5d5F`V~dDHM-V$RrO0Fch)<5_^wPjDlhDy^!_IYMr$Y$08dLAquQ z=|*Wi!cdyIsVlk1@)fC#CFzku(qjXpCvkR#Lh;3UQ3?g;Y0(QIq_N}-Hs!LkrP9*L z?d(d@yqBa!6G=;P#uC2~tBCaF7t*)V+QG-$@rN0xaFj~?q&Qd<(o;efcP?;IhHd!c*e7SVIt;U&u{JkNm`Zx=X-w`Jr54$wU z?S0bYA2uSlox@1or9JC2f!scqM0!4%G1c ztk6wcLiWWp4f2ebzFr@lp|^!}+b}wFC;2Kf_pV20*^$Sb<+PF1?K|mdGM8CrGf>5_ zI?@=&w4+YR&z zlfY>4m&j4M0eh@IB#~LnVN$T&rU@uGPd*N0b@8u@6>-1JTooMuGOJgnp&QmdL#;P2 zMQw~eqF&~`sK2!hdgb6z^x9E;U76GqXVF|Qj9i9>Yal_UGV}#$G`6|S`$QZGGVfBZ z;%j+lDFVoT%XI;m5O*J2W}?dD3jlJbpRGj~yv)UF*WtJLEP1<+UFg@gY5;|W9XMuY zjIs`)Z3DNk{!&0@u}qah1yH$7QO1l+Wt;$JW==nY(vuEF5F=qjOK7qC$*e!OF*Yv<2qRF!F_)UpAm;-+BWmX?&WAu4?PUIGs8+0W33IC5>I>hZ#j3B-KWmII-@r{6;j&j- z@a$ObVUD9~`HkJ!KB~I5*pjL?N3ma2|D3>Utm;3n8(6QI5`ZnX;%F#7W<^M}JxWi+ ztKW+EDK7z5Zptzv&CN%j7V4r89y0*cc04ZLf^nYda78b$ zu&*^XaWI~Z(kwQTpYlNa2ARd$I4X`n&A@3DsPjTRzUCuU9KD)-Yc>O{vD-8mAG6j`)VJt9`yB2km8bqaytA zOEB^$iWmBUZ8vP zR-z__PtbFvcwXy=W4fUK>eU7G#XD?&ga1A?k$%fW4|m|1Wne|#ZEL{b!~&RilJT44 ziaP*PC;Y4m+rz1t;;pa zQjFGg)^kTx;Z+~1_bv$4`i$2Z(|~V1=<(lc(E>KcW=iAq1MI=oPyp6`q*f2ezSy;a z+74i`csj;xu|f$)phehEgbJBnxQ!BJ$y~axz9= zxKBVO-gpG7>F4Cob?5a^^Ozy@=%wSRT^hM(M|Ku!Pm@LW6=9B6US-Qsx93%8blq0; z?zyK zz**>%3#KSNmV=gGHbI}IW1reSx#@sDr(tW@z9`;^J}txXY};71AAMN&1I>Sb5Ka9u z8_oG%gP#8*gWm5Zk6+Gx58&iX#_veZnZwb&WM9}@QvU)R@tA%E4N$`49Qm;FH11!u z?mQZx>xll`h9k#8!(uv`z7Jc^;oD(6caG{f|3>{iZs9TAd^6G1ARP0KpRmRXj&I_G z=!YxVn~n|FC!!^{)}vMV*dva#Vgh|o9)n)1BDW)6d*IRP-{Tl_-1g-XZYTf1v2--? zk1HD4Lq;Avh`GCmkny`uBt7wx>I)a*LB zz5do0R2}bM0QUtY7^nNh3hW~{aW%G>Tgz(!`sjlTdhIK@|J;u<)cEgs)V2p($?f1E z9^XfLjq;(8S)9%%`@*xI+KDcoPD3{;<8{C@No_K@UkiKROJC0xEiuNCe0s_*Cp2e2 zM&g}i?}$fR)hPYJ zDYUMFjb_y7qZjJ)(Ctn5>OSgUOYl`VKe4U6|Nf0dANOuTzYk%Kfw^S-fkvd~)XBaG z2*X+u_2gyhjYqiS4;~oU^Vt zdIM`civfaE@UBA$G7iCU8x#|D5KT{biQY|CM{nQAK;v&?+XlJa#s7mryB?~dODfY* zY7GOA^S%Mk?!eq8d}N?&iz&MO=R5Sje>gVI344E{EyMV%@G3I?@WZ4Rc9DG%mP*BQ zE%f;`4%)1YBR@<@4O=EGNb3jMs3!tAuWyVopRcuy#AopjV4sG^IAA0Z@+Yu;BHX>O zcOt$7lmbLf3B#6*+!y^2bx9hBCZq(T0hupQ_d7kP*?k-#kt&aHghz}$!DB`|tHtL= zTx^_(8aD4oC$-=eHT=b|lW4}jH|UYRi>Tvp1%97`0vZ3sr=)Rj$Sh)wsCXrh@t%%3 z#)K*3*%b3t4M%D0=Jl3nlKwe-$n?M_}7a@wlmQeN7vxdEKX#h<);q-B&wRp8gh!7t=7gZ9W~kWqCh;o~)|o}i7kIAX6^9c@Fq-24HOPkJ9jzn^mjNYOdpiYCUs zMxS3^i1w#pJEsh0ste$)dQ-yC(1qCh=^s}5;r=yP zwTJ8H^fgeiNg6)>wADV8u(ih{oIQ%i&z#`qiC*=N1IQY8?im_%0gsUNF)jcgd)j3@ zm$Ub#V{F+cZelNF2jye?XCE)dT(V8dmZA#J*W=^quW)o_g}%k!%v$^L3?8$EAx1AT zF_+8@U4QWvVIUQ)8|}rFpdMuWxywlJ9HYnr+@4Mu2DlY2_W=!?UxhZVz^hYEm==!D zoDcd_@G+ZAFsj=$E4@CbFe`Nu-e7;hN5jwT}6#3UGY#CZ|oi%Fq4!83_Xw&8!Myu}?8 z@c8do7=Nxe#{|{Jd)09zD;a-bGU@$)l<@%f9#L@Q+}$Fpjjo-SiTbPJ73prxI&7J{ z!+NvuF&5+})_eJuiTEs~1GVVIqr31p3T|7_6z^LAMGArVU%n_kdpd%8f1x(Z|=O0aPrzfoDfW zIC+h&czX|Ht`I#u1yDHw*V;ive+`bwirV_)sPBjM=)^B`@wo-xZBWhM_{g$1Y&;{% z9Jp7}DZGyW)i@J~>xrd3QYHPb@m!yRqx5OF3_k0ri((8Kv=sZ|X(l!wrZ!V&FJSF(*bHs z@8B6xlYb9;q=x?pbE#eNX1Onh$2A8di@MctWr z%-TWh$EsuatI<&Zk<6n0rc~)?rPs>j|Grl(GUHIi8F=ozTC!v_s=u1tcH49e&Dti1 zzQ^@ZP&ax1In=}+v#Y!6YL0&M!W>_3IC~e(ynsFXS`a6|S8=%-j()v?XJ5nCJ1fzO z`v(9Tr##}I_D`eHt1mE@#$;Tj290jbJ5i;UIrzwDKQXF?^?$HW>+5=OOx9Zr;`#o% zkB{eOGp_JQrDt~&em@Pshxg=q&i5JAK!A5?Q{B*;D%hUy9<8!Kzi!0#e>Z4!4Ii^$ zCr0w#WZw$(%n=+v?~7b8!uOq?Id~k60B_VY>^u4*7O$2~v##tzPi4rX&9_hE>+0P5 ziDp0Q2cXM5WuOi(ZPBzhiD)_<2Poa86(3KR|A}qbobdM*I;D3W9wBq60^KOUvyZWy zjJ#DSHHNgMoV-$fO``AtzBo_PLH8)(tABA<#}@k%g|#I3lClN+{Y%9TJi?dXd-d`0 zGKUROt>en*fm09A1b;iUG8D7;I*e-%phf-iYScF!+q30mP87cG?0f2{&m+9zwYEQP z#dtQoypCqP#Xe$u`H1bz_{?xdlYil}7z_VB#7E}!Vn(fVhq0Yo9tdzhlU#cM%tWcu z|9$N~#vJo@)#MFQHn9;5x&Yk(v6pV$;5>72T|3stpSj~Y2)%XjYgxQL#@ zwaw6ZAvhMzjGBZ#OvEwY*_>*Cva+$iy5#b)ue(;3q@(*QH=)tBx6y_-Tkscz6hHc) z5p9pqHs%2OgN4@+*17)2_?QnPKTroz9l+prGUma>q})2Gb4fW1$t%^sBpK|}{y;@+ zrGCHVm{I?Qb=c$mar)Q}{mCYH5P`2a zpqzjsXJBnI_Ta#_tZ>vO53?R{E5?2w2r0)F#_vS@4=rj4Lq9kF!$_zM9M7En%qQpr z)>X8ToP*-bAI-$aREV1a_@Bs_`DIe2pZf^uRhJk~Ua5F;SoMany)$skjF~RwpjKD=UI5T^5UgLs+G4aWA3l~@S`Psp1tBz zKF4#e$1l-8=aZkA#O2clVEI)yY;YEz=lv4hR$a35KtAX00&iE{TtA4F(KPRAyzgL5YEIbseRTV1)jmCxxnUC-y4 z^VZ?>iu;($t8ZI)8=nvV1IDg?;@Mh0m%e%hpUdCDJF7qY2!^cwXa$^3l0*=8I2m%ps3`PHws-FDmUw%dODLEG=J!;U-tdZ(Rs8a#N& zkm8V?i=~|%$925GkAfuJHQF^!l6aSR*J!tJ_g(hbZO=W14y_$FtgfzHKfFAA_=pjE z?X_1!!`^%E)6lqg(>~4nv^2H0?7MGUd;7?a{YQ@4Z`A&sqdGe~59mCwb9CoH2ONB0 zW%MBjcO5cj?AUSR4jq5!p%W)eoH((&`+p8QY|^Ajhfkh7Wy;j4Q;#^JXWF#h-kH;n znsM}zN6$QF*36@3@-^ew8OP0>J?r>mPB`(zlTJG2nzVMG1UG&F`E?bl@$xxW zTz2JESIxcp+PODebHlYaTzCET*WYmCjW^x&r$60%^Pg||^Pg|M_14>Nz5TX3Zku=e zop;Q?^Dlq7>#n=+x#zxnAH46O`xiX0@WDk7J@W8F3+{j9frSq*TJY$?zy9@ae|zk) z|NZ;l|M8C}7C-s;Q%@{;^696Zc>3`rPdxMFvrj+w%=6E^@WP8Pzx49UFTJw#)t8no zeeLzvUw`BEH{W?{+1tzCd2iW!?=D}y{JrJxzxTlhAAb1Hk3Rb6#~*+4*{3T%`||V8 zzxe!L|61|I$`xOJ_0>0Df6I%j92E+os{a3Kt#8&|jsJh3{$F=paetrxH{NKIjW^k3 z(@i(qY;*koJp$O<^btV)KS=!#0o4D+op&yk@W1Q1J`@OpD2n4`mtA(F1LV#iQb>%7n)c^Z5Ha0aiH@CF3w&H&X(9tn+_Xa_Oa)U3U59mtS%Dl~-JK<<(ctz51HD*IsiS1h}D(0Jq$t1i03dU(NtM;>`(;lf3W9$i%R|6@Xc#fu+* z{D~)?eCnyEpI)-$nP;AT_POVtd;a+so`3O0{QvSRufDnz|G)P78*jYv=9_Q5_4eEE zyuEDMyAa^L_ul_N{SN`~|Hq#~0sQ|3{$H_j<(ClP>#zU)?{B{O_S;nu0RMkH;FE!$ zul42H-~Mm_^MCDi_~#j{RqL)=f8bAk%=};fr|WIB?#AnF@{>(B+-wv4zxfuMZ~3#W zw)pv$TW|G?t#$ryTeJPI25q~;b~|pr1MZjk|LdKG3@#2S6&>6!{x5kY-wnLbk3#kT zZoBTW+tA&I?lBYsi2sMy)z%Ly*A1^9F`W6o_uh^BG&MFiwW#}B_ibrwX>aY=cVyds z9qRwlI`Jz99dhs?`2UcuF=NK*{GY)5@7DRR0pRdz061bA?ic6Jm~kZjpN0RAJ$m*r z#~*vbaVN|^9{(SA;;|?w0v-;hi&i`}I#rfxp`!Brc0_Oikb1uGQ zjuPMsoIm%f>zMyI|Jv)WyXN|9Zn);gxi?+&r|WLM@s^vJ|F<#q=iPDVy!p6){$J+b zHUI8A@455d`S;Dg|LzAK!2Jszeq_PIM;77!MUOt(KmQl2`xg@g7C*K4>BUQ)d}hhB z&*1*&UU)|R|I#b3;eXu!`fG2z`sQnIz4rF&1c7DmzC{>#=l!=oSoYyN|NMaY|K2Ab ze7gLzk3RqGi%)R>zgE8g<o6HEEJv*EOkoOix#D_ms(YM_12;@xAugiCsOtcK75%r}P}2-tcOy zmbQiw4b8P#=(z4_Q+vjDjjOTh+RC*Z<$AlOiC$X9_S$x2yQN(`e-|&I;rca*-mjsq zT&ZiUZEv@0>Kocy8*4jjtoDZH5sl?aeM3{Fy`$X9t$^-W94aH)%ANh!+#0KCWMfCe z8V@$Mv{l;5t#(b@h+&mcod?ud9kp#E${m%4=Jt-Zkxk|14!dU5khbpL@piXk*Q4^J zu34=;QwH00QzlL6X_`LyuF8`N)9>)Ma{FF(O>@il?hLDKFIO5EuTx_g zK`E-ewOrTHR@<1u8b>x^wPr?+a+SLJCfBaP$5HPtpY98j*WbhK1DD^96o*L0TKYb>GC-+Iq#L z=(u**>3oZ+M8NhI`+yu)LPq7w^yaTyzj^|p0I0LT00t=cpNq< z4e@k)YfCel)t8l(R`bZFVdb_8rXAi;Zme&&qpIQSYddNy!^J%vR(owzE8K+#LcTWr z9HF1XZ1MIe{p{4w1Jp)YbVf#dxue~7q97x~a%`MlB*^0WyjD^w`cBFJ-d(>Km%>uX za&s|Jkak$IW4Gvsr37>m6=TN*V~1`r31dNfxoD%sf0yn5ztDkQ9W@&LhgKLmp`&-h z(D!1O=NvaExlYLnZMT1nd1$AYltPS#O^YQjQtN^a;~)x4R%8c#pj_E;(Mw_{0PSNw zX07Vz0T-*qwy|$tH`y^b2~%C3#6E~*qRZVN@SGqiSxLrfd1w#UCJ`FjPF(a|S6ZcF z9K>-cDp{DrxW~YaF}#40yP_~~u;Ti#=#(N4oaJQMW0@)^3B-Lz=@aWsVY0g$gi@#h{F}`=Q_*4 zakU#Iea?!+c6LxCb4}>da)MmW7`Th!ec=k60fGYs?m3YIkt`?7b-S?#BWCDgFb8Ue zj_ARpb4i#49%W2t9B1GG0XPh0>{#9v!z(-_>k@d3Qm8~%qTZSw5OM74gvx92Xu#l8P!#(-3EcDj zBw@0;4kU)Hw7j*DWFoK~qAik^Xh@Op`< zLW}v5_yUJ@uqnihm@cPPD#CbIq(h(}wUL7rd`DR466QR*Rnz6Rph%2VffH-dT3W;z zi9ePb2pu$Va5=A4i~_u*pu`uhzyMqr$~>^#koBTS)~(9twA><^iDdx=j-#w|i(#na zwOm-26#HnC(}MGn8;kPLFLA}gw7iHAB+_HsWhlZ*`p;>3MY!UL@{o%7D3iyBN{P%u z%Oxk2CsKG$i<~IIOA3m@NR^kcE=*!|LS|I-B^u?o!eR-LSd0)AVR+FaA_jQANcSAu zO;|e_9PpeLkCxI%3nyvG*kdN4>&b-mT-$T71YI$@oK~z(k`X6#qX?ms=RL#$=!smke<`xQQy~438h0%*ljy*(QyP~d4U9A z=A^=NS~?HZ-cY&}ORy7~hoJ;8%L_?Z(T}T0&uQ_fL#P#yUkqId&q8k37b;nr0ejv+?IpmV_g`T8%aqCA{I7Uf;SO{DXk$mPR~bQQ~M$?_hjGmZ%#6E}d9X2rV3`HtX3z<=*WA_QLjs`0qL?n~c3LHDcwxS8}oR&{$NCPRLDDW_t zu+xvEWd*JsF#%XNxt!NRX6Vap0R<&rTF}cUzYw@*N31?9wgS&-F^$;#isJxzp4i(C zpsyqsz{q$aJctVmILHW&jg=R6|92Bzr8~BIs^wjTSPg00>z^;!YshoE@P& zRdj*JcI=~+k~Xgel~_^4boeDn13Ia*i|quS*f9|_QqtzNkV&lpGuaVhT$w0hEyg2$ zCtr)PXgZ_wTFCU1K2S;HxC}RPL=h`=Y<7rFnknbCB36_{!hs#bd9fTVsfj3ULh?GH zrII$U1s6DKkg>$v>_dm5lhhYS?^eH zuwf)bwIU}2%7e4MVRNo3U3q%>_ieV<%50r)1tyAUOrw0rF|(eB@`~MvshiqH3Z`B#T4nrE#7Rfchex6}Io$ z%rN>c>9Sl(Gb>`QPiR!mgOjk3?F-ikQMwR|bey5R?%sp|S~-)Xl$kG_kF&KpBeSnZnVhBS+Co5P<0`8R!HVrW`#UQR zb5%ASF?0fpEE8}I(unC`5>ZFT&JhE-0^{IvUW>JXjhr}6w%4()uvEd=eoLr~1>4p)CUgzNR2cu$Q;(KP@H6FV?ddfWHge5=~gJwZ6F1=n34k0wUE*0E@N+FI^7{? ziI93-$i_lCV&QRQ*K8$D#&ia5q!Z^$paeuuCE0Gul<{N9@vX$o^>zj>q}6D{28bhm zK)~6*OWd)NEbDTmqjE`{B$1ZXTztT?SBfPeu@dsSeuCz~WmE1-aY47|sSk+Vq_hmf zO0ujo9U0Tfo{e)9;AFQ%eBnGRho|AwO0ujoa5HwPXUIHJIMYP*kgc7g+y6vamwGNU zj0~K5Mj#~~Tb3(z7jOc$R!)*E>++iqH5S&Aghl*C*wcE8@CNqFgeOnQ#hcL+s;ear@L{ABqWs`R zX|=MHC7zL=!4qey1)GTEO2{td3l@8HLVlKcd3bg)L9DARnI$r-_%MxkoR?&LmWK}{ zvCzDPV9)X<=0`q9+o}Ds+%r})B$ji5bmAbM;$d|TP;C|J5|DTpon=)Jo*qk>28Y8CU|*xk(GL#@aG8}JUmXzhE6G);pO2; z6gb@!{fRpynqq&cVtA7Aa?HT>{uIyP4U_etKuOOuB!`#r%RbH5utyy;gEy?n;0ZJG zP6AR{8<=z{UN;*eKbzzk`KBjghwDjBgdCWF(ZrGw1uQ%Xo*iv}=rSndf38vU6{|~4 z*Gn~#+DdAFCwn%Qho=VMQ|o%=it;w5X*dS}(H+WL}_~G>Su%AJGIn#})nl zHz?EcSvf-WKanKTDuk4=SO`;mmVX9id_tLuDwxb#A|aWcjD$JurMqgD-pu^Vp!%pF zg=J>Hpordy)}Q2B9LrSToGZJ>$iq|HWoN5v53Yzc&t~JmRvn*$&+;!1kJLa)5@=0& zUY);eJR+?{IdW#vZ@)Y|^(~hQ1bB*8utz<~zFz9DB0tMNgEuXTO|08uKMD*K#iSKb z($nG!`0QDk(ZqOy%?at9@Rw==o)q~~2-K4b@RaiUZJmebF8Qj=U&;wge~~Xmc0F|k zpXFa3p7AH7Sc0bnOjjwMt&heZ4lWGJw2V%&YRL{$cjqoIHVIzJ=Mu@0&&fsq{O=nG zW9`32{H3mtdDm=uEN=)%a##lw2usY5F17AIBG6L_WsE%2O}^~=h`lR9yp zEKxIEwS)=a;cqHG%Re&`(ub0WYoBil}P!=i1o;U@n`v$C!g`Bww6^VeVak*F~nbuUmRc< zs~P#GCyG#suLTYkooZ28);~_ZRR1U!Z)}3q36(nkWS^{QyVzfAZRj6o@J4C|FY9Nz zYLkZQnUB=Y(J1v_mVbG8R?T#Sq7{~E{9{lgzoI{f7zSlpMn17F3DQ+ZHY@4+D|IS~ z$>6j6%fn05rXqs>U{c~~Pw3B*ruuV?LA_B6&UiSdMH!jBAlGXU9{7L2K*I6=n=3iQ> z5bK1>Wb{wuNKNEt`DakZlEO)SuRzZL(xWtdqSbuCXZeSgsVy=-l{19&+?G;FAWm0L zlzgq{Q~1f)&)|(FY&u9lb^gigVEVoZm1>qho<@F_f4O-0n?@~aEp($Z0VYIg{Ngyn zj5QCB*U4T5MVXaKqcDk;Ow$MCbDm*z&BL>P@ivp<*+lB9BX~bOHRlb6TzsY{=4x7A zO^Ae4sF2UDfeEDi;Y7nk4Z}o(m+i8im6A0Qbk%;;c{xYp8T8KU{N+%?NHr~^Kg9+&sGhu2D^Mn&KYq~o z!KsEp8A}*E3B>$ELFvX{@5P;9t`f~A- z>j1)Ql8I^lfP9X`b^da&VQiI$Xa7R}re23XDU-oRWNu=APBt=Q?O%z_HHliXRA7Hj zbt3Ia&I6(}{&BRCdA84#nQL&B{7?Kx?Sf-Jy0Te6^<-D%XZOEG|ICq%o%DHXG9f+CWoyDqhEEVVG? zXZdHWX7Ex!NwsDt68mYn747uc0eqH!xp<_KWg}mYK{fxC(;YU@BA+7;gEB26pS6M) z3nl8w@nV?1?+0GbM!{$KmxpIfMVwfjNXW^W*iTQF^f;KEUB-S!s%gO$Ug#J8Qa_>m zqMj_V6;1b#-#O(lDAQuE=}T2IYZ5&OR=l3~^}m8^@lO2Mi9LtQyYU77#ymeMj-Pq6|9XW& zGd~mRDt;cr_TTU?2pu-Ad#t`c&jTB#zX!-SKWA{j4t(=Ba0l#G{cf#(^Y1?CeSTI* zf&K>Y^C1iTl-BY-$Sc^9#{Rd=r8vLC-<-Co4-}K)57vQ(<_aQkYukDNHGhD@-qpEr^8rLU&;rS0@#^ z3bP7zh1xxHp;WJ>2Z-e-^kUm+@H+-@!V@g z@))GaRhwR?6b?s%$SD-|qR2}To8zH+?!6T1!TFyn()!r()S9_}z zCa4_IrHgxlEfmDM^^B^6-xYM20C-fIi(g^`6eTJoo6Fctb3Uznl& zkLS}tXVJ)5EVbugu8*bfN&N0X+v$wwFrNGV(*?Ev_`;#wn~5C;_1k)IM$5tfQJcYQ z*kLeQi$z3lX?3FW;KB&nO^6c z@~}&JumgS?iL{vn6Mce>!88=WF6nDXS90e6b7#AFr$8qcbp4*nzG5XK*Px{<@zI)Dj&$qqj z+;dL}DcVyLo8Mq%32AJK#{%VL?5yh9Z0bp3A!#I+q>zG|+SZoQP0=OfSAS;@p*MbS zIovL1<@8sBs~Z;-rQUm{tnK5;LoSdCs97mua7ZQ^3ojbC~-7UK+Jm=)b zNW6xSf;>W!wsiUhB<%DyH$~&kv9Q1v3#`bCih~y%yv_=`gO^z^D=l%g#TG@zctf*H z0B(+-I*6AvlO_@+ag;FPeh^+2TH-b>u3{*Zpt!bFD{8JAuiGe>>?5& zV^9|&=M;1+`u~>k$P&bc5MMJ}+uX7=7M))o=U7)PQWKBVv5TYede$9nZE2`k3R18V zXK;ed)-=_z&e;5=4GpZz8$_+d@ff35tonDwiWN#(e_OnzEzY{D974C1g}(?3b-NVK z`r&Atw3v66xJea4aSqlE|1U~9FFN5TR+xOZ60+?8AtSycBqys!$D(%$(LSLaYrzbP zz#QqkSusK~W@jeJAq8YG83y+iGJ#AXGe|92NLG*=$Y$~|*+>^;{pN&-T8< z`fkcD%5Kjo%-N7zkh{KLe!up-fq9$qi}UXa@JTMZr5mr)Fp~SIzqSZ0(};7oWXkcKC^!!E>*v{VWoix4XV5di7>IHk|hT(Z(BC}%EQaoUgf#^+m&~&@?Q7D>K$ul{Nej`ci-s0 zspC(NtdF)A-}2Fhts7f6xi%NxdHU{y_dIy-pZ{n1{f!Sq9;|)1?$PM3*kd<8zURp! zPp3VrKi{@*=ZjzeRem{saOZ1Zzp1```JqSO{rtU>_vatk`r(JihJJGCr}v!t>~rRkdDhy~cXK^=<1G}&HZkpwX(!Uhr>{@{obfQ5 znIAG{XFQQPFmrk4v8>9hJ$**@Y44lfcUj+mW=FF>%9)dMD0f=!!G4wf4(3hEJDeZR zKUuJ}f8YMw3d;(Q3|LV#tmweN)ZA?Edo>+eh1v7T-2(=J0~y zhevE2Sv$%x>R%-ZiEl`c%C{)X)rHzT{Ssr=xEbfq zx?s-u$c4=lmQ7sa+*tOoYrp%{r0jBIa$UvxDQ{Hfc!R!M{YR&j1QrEfm|i#|GV|eT z%k0oa_st=fREKxfAU?MwN~4=!u` zUH0;)u8LhVX61=h_g=s7_ho-5So_5d`)|7UPpj8Ax6izF;%&vZr*As5`RH9QZrQqZ z^|pog&DrkR!9O(gk=&gbyYu%HKcPG|_2~uA-v0cXFEB5;|GN6+cU~=iz4gr(-WhPX z`EM`&edq^EkG}Eo=;NzS96ROu?5;0uU)7&^?X2Td5czthvjKVuI_W?ql`+Y)w zBzN(nKh}@oSSVHEB$ePtJy}eyCmYCi@(g*4d`dbj1(vav3QL{kD$8ceGnNmnHtPuM zRBMa1-TJKcq%F@j(bj0&XnQTmnxrQ+C2dPOj@6?!c}wyqDWg+bQXWhBDbNw3kKenB37AA|MBt|+d zA6A}McWL+QcN?3=Z9M;u3$~2ke&Oy3`zF5a{G{v$cfQA%R8zio@{<)`Om$Q?dw2PM ztSX;&Q{aQp*y&f#cx_fub#(R<7x$eLx#a0fi)t>Pd$cYPd1?NH`X?{DaKZk@P}8xN z#j*6(O>OF;H?IUQT?&UPo$rm^^ZF~KYH5!&$_SgJ$vF`CEqvx zxU)Au@HW{x2Ky93de#*B)cNavx4S|W63em-Z)hF%1=uIl*%?>B;|zJ+tbHc-3{z^F zSe9;z#_`HHkvBI=cBr0F;qruCmCj(0wYw{VRh781hu1NyKn| zZ&kQF;HmDeQ_RXre;^$2RIzsKZ^Q7{&V-zSa!)8+;R}WW)4d*Fh_%mh1fuZ>8x^{@ zEmg7RQr6Yn&>Zu&H7$sCSHq%C8^WRLDi6k;6z~Kmvv!~V-&)F?K~J~>z6o}wM^Ps# zSmkkr0@xRsu*&IPM92p(VO-ed_KK`s(gjXaRY8P4^)BtleJ~s_>#aXl2q2*$r0teK5`KFrhCggfiS{3slrp~4zgNTu-(p( zGdzi=B*X-r-YRetxu=8+nD=t?Zhl^cXVH(WaJ6}#Z3d|~x|fciCxlJ1+)D)`u*fsV z(_cc#;5k_|bmix*DyNC6%rjy_OwWkFjh6lcw4^KDx&>XJR+zdqo{|3pw8{%=m*v#n zZZs@c*enB9{-&cmhf0 z6r`N`t|0xYq9cgVq8K_&x}dNU6u=KUegaA5L|Ii7fK=p{hxa_IaiXpox(K++N_YYV zC_I6rfrh$)rJU6{O~GRhMBlTlE6ciF4F`H91T33HzLPts!X-( zcvXOvCgBrF=&Fh;ct`nRh^Cc-%o$22Z46d6IG#oh@0O4A_Qznsw zWWr8LRYl`;(EvYL6-z5G>R78poz>}BX5@)L=bWX83s8afqLG@J#bJ}Py{;95HLZI4C*^vM?gSYN$#bo2h=kVmT_H^z;A$b!BBK0 z&Y&x`3Z9?af-uw?B?rcwj4xFx%q>U)k zaDwV3lW_Xlf^*bnQil_iS{yXu=nf~dt*C7vHDoDz(J$!kzH`ymg!&j-eyblnmyKbR zsCm*$aAM051D8N#j24Ba2*zo}o!~&Tjm(8MT1yW&n@J-yP(3(GL|cv77KcV0<^xfO zwl+w9AU(uKDqx)pe$=3ro+wjv+#D^8b0MmS+Cu9nmf&QW`sl=&u@k6PAm~XlwTR%} zsckM~FGhEgAel}Akm!>Sm@vkO;M}>>hbTN^O^bRW))`NFbX$wD8*s&7Qyc790Mx(t zCT9F2WFFdCr=@g3}yRkB+`P7^0x;ZRsL2 zf;8I^M4CgZgp&SmK*gn-cg@r&zBJbYQ-v;CCi?4b|1WK*ffdadD*`*ZN9~M(MvVUjWYI6m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..101cda87b918ce46e4e81d02666939e6743395e5 GIT binary patch literal 379 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6n2Mb|LpV4>-?)JUISV`@iy0W0 zH-a$Z*XFmLKtah8*NBqf{Irtt#G+J&^73-M%)IR4+O^%(f-l+hiu6wX%i8aT46s&AnwylHhWombUP@#Z(q2;R|4G*L_ z^cFQKaImw=wivH?@uZ-09=lvcU(B7`{nbCEe_X#A8JQpWL?n5-;r2r=Tb1TqEc)=) z@%X-_tEN70l;?Qkc)f4Jr!NQhGZiu%;|bWBc}d!j)sa)`&c}74kGb4EKKS;`xh5`N zlEW$VCvZaKC$2pgLKiSO%#<<@sbErg$EvAk`K)CsQ{Rqf;Z0U*O#i!{$0qe zwr7&!$w~eqH$)2<_Y@vcR(qbq*DNUgZl&QRyB|@09V?a_6rVnDHfGA6#p^f!*nTao WPkY`%uA{(EVDNPHb6Mw<&;$Tq&X)H8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7bdd7fe20b2a46bf16d76052889a08d91abb722a GIT binary patch literal 609 zcmV-n0-pVeP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY2VwvK2Vwy@dYRh*000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs0005tNkl2P#+)n(o)k5Oj3pK-b5VtI`CMzCV_|S* zE^l&Yo9;Xs0008%Nkl^rqZ`F$B;AEH2u9H#BydF#{edy8&0tY@QBl|!2+>_qkQ;7VYUHw8xQfpi0wpt^K7ed(Sf_Zs1XV}Ixpv4T+CNnqXU$~^U7*Odl zgK9*MHu@SqECxKrq!2c1&RCPsXpuS9%Yj#TQ+QUEU3IIyw%8@8<9HwufoQ-Rl2h+K78VAlGgz^rN&UR21? z5yy1fAWXpiqQIE2L$&o(<#&{sF*r`GJpAI0^o9K&5ZW1$^NzP!QU%pi8JU zQ6At`tK8!Q-$?)*6)L<>d4SU*sUQLL3l-j@T)>?BgjS(rTZt z2%O-w&}(JjS8F{D3Gk@Lg(~Nj1`aYVY|x{iU?>GRY`bb<*wQVU@vu;1q9jmjc3y{& zq&)>_5=o<$0It{~)cZ*~5V|cg>P*sSDL}z?mBKmDCp-y$*C;fX1VWE%tb7JPTI&cH z)9ftZk_SAmG!Pne>P;F-2RNw8_W)qV$FL%>RC_vb#l6Cv#sQ3IQQ>5HfJOU7Qk7OG z9MfvGNa`m)@VAGAyS<~|9YR>|1)q9Kc+hNqfIsy46TB;gF2Il<$O5GA7a_a`K)C_B zbQsG7?Bb%Z$)fCgMi_*bmjg9!ENT>*G-qtH5T;;vSl&6#YZ5uCF$kAchUL}yjT@#+ zW^Nj0w1~8qfnjsQ18a9_ZZ30nX8`S6Yi}Dj7X98|F{^%Qur`O6Z>n*N)M<(W z>TZyvE%sBPNMl0_ z5-7Vs+o=Epa-uw2OFP{G8`QK09{Nv^K z)$;v$_44QM20xxwz2mz6RiD=N^Ec0{*T4KwDJe-tY3lm>zxBT8U947D{kp!nxe+&7 z%opeNn^!f<)UUojJ;sFN?F4A3p_!x@uf{#t!^;;{Rb4cb^NZC<&&eLY-b}`;i|yNQ zQ1N0iZGQXh48PBYmvDDJ9WO`2X;Tlcu9o$?o5@T-6^nO+X5jXuw|@Ard6?-1j*iJss0iPyuyE5@eLrB^XtW^Im5h0%$gNPx*a@D;TCEpvv=_Rs_9tW zfXOyTxx5;Vnq|Ggi}@d>01vrLF~&NLCh3g>{cTR9&?*gWw3b6g7B&{+vpX$FRV#H2 ziR0z%Y&HCFJX?NQOdXB#%;v3FH0$J~`J+)?NRG4Q)%9YUB*vq3G9w@&9R=7)omtbu?nnp zJ1!58FE57U`OQgB?GFAvpI@HzqKITEsJ*!(@LqdCPKCHX$o?2%ypqB14kE>{c-)pF zNoDSk*w$~1cSvyU-7y3Y_T9Q(EWm){>F~B$w8dK0X`RN+*)p#Tzv<1;)o^u@hxm<1 zZlYSQZl@r|*&J$@li!;@DIwF(*XS$L{tSIEJsmHur%k_k-^}LY@z=}MV*ak_w`<+L zu2a(Y!X^cF1Eca9_}^d5uV>>D{O5c!>!O#FRkN5*@JHXYe#p&O%Y;5{2ih1A)lZcZ zN-O80JTk&Zl1`fHw6a1;Eftlh5!xG)MjlxaypvLg0`o#C?Yyzopg?sHq$D|l&H|0X zI3JWL?MPv*@?K|;GR8W!Fu^$EGRiuQuw=Be1!}C&Mk^b$os1P84KiAIf11%8XSS2` zkv%G~!NGj~UTrvaelL4^nR4gk47Km8#c;O7hJez%9Ih6V4_`?Uy(XoOWIHuK6hncm z@c|E3DLD2db*D`3F1?*jJ*IS#DZHgCzwFcLFwHZsm4Gu=#`pkUvl}Hxky5f+9z_wO zG9k*uQAtRD2h}3l5Tut!1piFXx`Qd9Z45?Mg90^580B?<&JxuE7<3kP3@@qiT4s+j z2Avjjy!EC;;U^RXuWX50CkPrtwv(~aqd`Us?@u$Dpbjw36-SFUo)Y{+Awy?*xyi=D{lfv73 zxYqx&71-%?8VU239=?O`^SsMRWEQL89B;Iav{gQ>7N5YC(`gMgMeAhs6QjyTFIg5V zWut>^){tfY{3VEYF2Ms+tpx?6om2n*R0l%(bkRpssd~H^@}@+6K;hU_fW(IzqdOQ9 zjz78l3)CuMj8zdjOVkK*6ud0#Sk#))D8TGd#-Ni_5FSgKQPxRX@_tp$2Zm4~n(bt) z^k|UL!u!*V<~XyRoR92LfejAj^V7@!*g_d{hbJwZH&KRr*=OW`Kl?u4M&Xp)-zZ3t z-rFd=_SsIxvPYk-aXhWh@4R}E{(rlAkrS4bRPl%+O3*8-xY(>PLFqsRN=7)VjWb6| z3e*xsS7IcBkHPtBkWnsc@13?9It$bYQbv+1>{z~qh3IVdC}Zuh)?tp!DAR`zv)bzt z#gkLav}`A1g-3&o7T%v`G{>3kJZ*p4R zb@f^_qg7P&c=LL^d41-aS7w`fym&ocy#Ab1e^mPM-t`~5cX7Fsb@X`edc1f2x%aNj zJ{unIUH=pJuKUl(`$y#TI32DUc;GZhW{(WCBLveN>&_uKM*XpWe!$AI^kEN_Derh& zcI=;2_DPfBr0#5ZIho$}e|h#p^@{n?jH~aa!_m9y_3KHO5;#sF>(*-CeFH_E&YC5B z90Yk4h);$)xC@9UN=BVEHW;8$5Cu{`p_+m-E}@27gHJjvstW=!;XzuLq`9L+BAd|Y z!84UmNj|tP3LU)JJJ*$F6xyN+Yt7`faX__z|KLJGP4r*|bTF=?+UO?lyrLwn_{{23 zgkD=srbNqp3_7?H(Kuxl;|jSUZPy7=(YNX2zGn@&1jFZu>;be+DknRkAQF$N+=&~o zwaOU3BO9HwB-^NUS`*93mtG~%1gnz=fKeqI1%%k40AR2l?yf^Nnv~xG`IegQMJ4P- zR4EYvz0*3ZL8oFcjzNnMT2LG5gI+7pA#7x&jaCCrmkZaP?>()lV&E%sNDOY1IH;r` zofH5k0dmOp5VFlVk_T!yVsj3aLC!fwxvQFxI+^h>$Cz^nS2Jc1qqdAit3cfKY>SFk zimO9uWY)1HQUR5Wf%S103!p=qU7${2Dtp8lWi&oA7BF2!#z5ruUzo{g8`U~dkn$=r zYT&m~X;lpqh%t+rb16-M02Nrv6pnjGQV|m+Y4nJ4bEkFDGp40?wk4{U8x%S9o+Y;n z2hX98pb?ypoXHfSkwAB=LJDo2kwJC_%5HO7!rU{`(v{^2N+|1;?$ol+i<}DI8E<5Y zB6<^pWY9!oRLChP8cH`?sr!(YdPg>Tc8ftHEj%l`8ha2-WK*YQ3xv1MFayeNiCORj z3J!JLd(SLBQjq50w$~2nhzy|Kjx-7oHKq1XEaj$i-v;yW`w5{!Rdg|ahGPqJFs36$ zAVFB?RPO738-PX+y86vw*n*>$WkEeiFdBmhjv_G{)8BqDO416a`G%t?a%ZN5DlE3M z#3-h<8c1v2aRO@y!k|hL%BLTa)U`$)Hl#A_nG+GTNN6NJ3&$N+M6(ipf6GzxH9n)XWW zW%Q6WR*-T;j2bE6XjG650vV!^WUWxr@L?@z0sBfOS%jzz=Zhp9F!o9^qWQ|t`i>Dn@3hSX=I8w+) z(8@mP?H=5`v^(d>%Vsqk4_Cu{_}d_)Hb=l$&tH^^@JhNJ!|1R0$_~%%&+zU18XttQ z6sE?_au z{oRlcPTvX&&QHZ(Z6bovFi&kkeScJi_L&iK$L#rSnO;W_o|6%GPFvc;~G)5)lr zE%)rSuy!9el@@&Rz8TFg@qO_!S#75->%k8%2elGZ=S3>gcPN27J!qq~1jt0MFha*Z=?k literal 0 HcmV?d00001 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 @@ + + + +]> + +